From a02d750c8b998d212a7c3b41029a7294b600a3b7 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Apr 2025 12:49:38 +0300 Subject: [PATCH 01/35] complete region integration tests --- src/Application/packages.lock.json | 9 +- src/Configuration/packages.lock.json | 46 - src/Identity/packages.lock.json | 41 - src/Infrastructure/packages.lock.json | 9 +- src/Persistence/packages.lock.json | 13 - .../RegionsTests.cs | 1393 +++++++---------- .../packages.lock.json | 51 - 7 files changed, 609 insertions(+), 953 deletions(-) diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index 0718004..f56b518 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -59,12 +59,6 @@ "Microsoft.Extensions.Options": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "System.Linq.Dynamic.Core": { "type": "Direct", "requested": "[1.6.2, )", @@ -179,7 +173,6 @@ "domain": { "type": "Project" } - }, - "net9.0/linux-x64": {} + } } } \ No newline at end of file diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index e591490..7bd3fb8 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -150,12 +150,6 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "System.Text.Json": { "type": "Direct", "requested": "[9.0.4, )", @@ -852,46 +846,6 @@ "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } } } } \ No newline at end of file diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json index 71a67ab..aa4f436 100644 --- a/src/Identity/packages.lock.json +++ b/src/Identity/packages.lock.json @@ -62,12 +62,6 @@ "Microsoft.IdentityModel.Logging": "8.8.0" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", "requested": "[9.0.4, )", @@ -601,41 +595,6 @@ "domain": { "type": "Project" } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } } } } \ No newline at end of file diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index 3fc0122..83c6755 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -2,12 +2,6 @@ "version": 1, "dependencies": { "net9.0": { - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "AspNetCore.Localizer.Json": { "type": "Transitive", "resolved": "1.0.1", @@ -185,7 +179,6 @@ "domain": { "type": "Project" } - }, - "net9.0/linux-x64": {} + } } } \ No newline at end of file diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index f9da6c3..af85452 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -51,12 +51,6 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", "requested": "[9.0.4, )", @@ -341,13 +335,6 @@ "domain": { "type": "Project" } - }, - "net9.0/linux-x64": { - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - } } } } \ No newline at end of file diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 247d706..16c42eb 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -207,11 +207,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new AddRegionCommand() - { - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new AddRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with user role (copy tests with admin role) @@ -224,11 +222,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new AddRegionCommand() - { - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new AddRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with unauthenticated user @@ -401,12 +397,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new UpdateRegionCommand() - { - Uuid = Guid.NewGuid(), - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new UpdateRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with user role (copy tests with admin role) @@ -419,12 +412,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new UpdateRegionCommand() - { - Uuid = Guid.NewGuid(), - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new UpdateRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with unauthenticated user @@ -504,10 +494,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new DeleteRegionCommand() - { - Uuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new DeleteRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with user role (copy tests with admin role) @@ -520,10 +509,9 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new DeleteRegionCommand() - { - Uuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new DeleteRegionCommand(), + TestContext.Current.CancellationToken)); } // TODO: Add more tests with unauthenticated user @@ -531,760 +519,593 @@ public class RegionsTests : TestBase // TODO: Add test for GetRegion and GetRegionPage - // [Theory] - // // Empty - // [InlineData("")] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // AddRegion_WithInvalidName_WithAdminRole_ThrowsValidationException - // (string name) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = name - // })); - // } - // - // [Fact] - // public async Task AddRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task - // AddRegion_WithUnAuthenticatedUser_ThrowsUnAuthorizedException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task UpdateRegion_WithAdminRole_RegionUpdated() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name" - // }); - // - // string newName = "Different Name"; - // - // var editRegionResult = await mediator.Send( - // new UpdateRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // Name = newName - // }); - // - // var getRegionResult = await mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // Assert.NotNull(getRegionResult); - // Assert.Equal(newName, getRegionResult.Name); - // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); - // } - // - // [Fact] - // public async Task - // UpdateDuplicateRegion_WithAdminRole_DuplicateEntityExceptionThrown() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 1" - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 2" - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = createRegionResult2.Uuid, - // Name = createRegionResult1.Name - // })); - // } - // - // [Theory] - // // Empty - // [InlineData("")] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // UpdateRegion_WithInvalidName_WithAdminRole_ThrowsValidationException - // (string name) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 1" - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // Name = name - // })); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // UpdateRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task - // UpdateRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task UpdateRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task DeleteRegion_WithAdminRole_RegionDeleted() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name" - // }); - // - // await mediator.Send( - // new DeleteRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // })); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // DeleteRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty - // })); - // } - // - // [Fact] - // public async Task - // DeleteRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // [Fact] - // public async Task DeleteRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task GetRegion_WithAdminRole_RegionReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name = "Name"; - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name - // }); - // - // var getRegionResult = await mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // Assert.NotNull(getRegionResult); - // Assert.NotNull(getRegionResult.Name); - // Assert.Equal(name, getRegionResult.Name); - // Assert.NotNull(getRegionResult.Uuid); - // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // GetRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty - // })); - // } - // - // [Fact] - // public async Task - // GetRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // [Fact] - // public async Task GetRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Name 2"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 1 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(2, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(true, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(1, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 2, - // PageSize = 1 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(2, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(2, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(true, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(1, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Fact] - // public async Task - // GetRegionsPage_WithSearch_WithAdminRole_SearchedRegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Some 3 String"; - // string name3 = "3 Name Some"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var createRegionResult3 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name3 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Search = "name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult3.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Search = "3" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult3.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Fact] - // public async Task - // GetRegionsPage_WithSort_WithAdminRole_SortedRegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Some 2"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Sort = "-name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.Last().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Sort = "+name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Theory] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // GetRegionsPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException - // (string search) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // Search = search - // })); - // } - // - // [Theory] - // [InlineData(int.MinValue)] - // [InlineData(0)] - // public async Task - // GetRegionsPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException - // (int pageNumber) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = pageNumber - // })); - // } - // - // [Theory] - // [InlineData(int.MinValue)] - // [InlineData(0)] - // [InlineData(51)] - // [InlineData(int.MaxValue)] - // public async Task - // GetRegionsPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException - // (int pageSize) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // PageSize = pageSize - // })); - // } + [Fact] + public async Task GetRegion_WithAdminRole_RegionReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Name"; + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionResult = await mediator.Send( + new GetRegionQuery() + { + Uuid = createRegionResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult); + Assert.NotNull(getRegionResult.Name); + Assert.Equal(regionName, getRegionResult.Name); + Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); + Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegion_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var createRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(2, getRegionsResult.TotalPages); + Assert.True(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(2, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(2, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.True(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.Last().Name); + Assert.Equal( + createRegionResult2.Name, getRegionsResult.Items.Last().Name); + Assert.Equal( + createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + } + + [Fact] + public async Task + GetRegionsPage_WithSearch_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var createRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cOUNTRYn" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "region name 1" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + } + + [Fact] + public async Task + GetRegionsPage_WithSort_WithAdminRole_SortedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var createRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-name" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "+name" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + } + + [Fact] + public async Task + GetRegionsPage_WithFilterByCountryUuid_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + string regionName3 = "Region Name 3"; + + var createRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult3 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName3, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.Last().Name); + Assert.Equal( + createRegionResult3.Name, getRegionsResult.Items.Last().Name); + Assert.Equal( + createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); + Assert.Equal( + createCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetRegionsPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetRegionsPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetRegionsPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegionsPage_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetRegionsPage_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) } diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index aac2a27..017ffbc 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -11,12 +11,6 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[17.13.0, )", @@ -1025,51 +1019,6 @@ "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } } } } \ No newline at end of file From 16457fc2cc3709b69f7b54118d465ba7d23308f2 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Apr 2025 13:41:36 +0300 Subject: [PATCH 02/35] change default listen endpoint to 0.0.0.0:8080 --- src/HttpApi/Program.cs | 8 ++++++++ src/HttpApi/Properties/launchSettings.json | 11 ----------- src/HttpApi/appsettings.Development.json | 13 ++----------- 3 files changed, 10 insertions(+), 22 deletions(-) delete mode 100644 src/HttpApi/Properties/launchSettings.json diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index cb281b3..9f72a50 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -7,6 +7,7 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.HttpApi.Services; using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; +using System.Net; using Swashbuckle.AspNetCore.SwaggerUI; using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.OpenApi.Models; @@ -90,6 +91,13 @@ services.AddScoped(); services.AddHealthChecks(); +builder.WebHost.ConfigureKestrel((context, options) => +{ + // TODO: Make possible to configure using file/env/cli + options.Configure() + .Endpoint(new IPEndPoint(new IPAddress(0x00000000), 8080)); +}); + var app = builder.Build(); diff --git a/src/HttpApi/Properties/launchSettings.json b/src/HttpApi/Properties/launchSettings.json deleted file mode 100644 index 97084c0..0000000 --- a/src/HttpApi/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:8080" - } - } -} diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 982fe9d..4b2b05b 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -1,11 +1,4 @@ { - "Kestrel": { - "EndPoints": { - "Http": { - "Url": "http://localhost:4300" - } - } - }, "Application": { "Logging": { "Type": "SimpleConsole", @@ -15,8 +8,7 @@ }, "Datastore": { "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", - "PartitionName": "application" + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" }, "Localization": { "DefaultCultureName": "en-US", @@ -26,8 +18,7 @@ "Identity": { "Datastore": { "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", - "PartitionName": "identity" + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" }, "JsonWebToken": { "Issuer": "https://api.travel-guide.cuqmbr.xyz", From 0345f58f7b3fcf96287658c57d5d41e521ad961d Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Apr 2025 17:29:40 +0300 Subject: [PATCH 03/35] add city entity management --- src/Application/Cities/CityDto.cs | 39 + .../Cities/Commands/AddCity/AddCityCommand.cs | 10 + .../AddCity/AddCityCommandAuthorizer.cs | 31 + .../Commands/AddCity/AddCityCommandHandler.cs | 61 + .../AddCity/AddCityCommandValidator.cs | 27 + .../Commands/DeleteCity/DeleteCityCommand.cs | 8 + .../DeleteCity/DeleteCityCommandAuthorizer.cs | 31 + .../DeleteCity/DeleteCityCommandHandler.cs | 34 + .../DeleteCity/DeleteCityCommandValidator.cs | 14 + .../Commands/UpdateCity/UpdateCityCommand.cs | 12 + .../UpdateCity/UpdateCityCommandAuthorizer.cs | 31 + .../UpdateCity/UpdateCityCommandHandler.cs | 55 + .../UpdateCity/UpdateCityCommandValidator.cs | 31 + .../GetCitiesPage/GetCitiesPageQuery.cs | 19 + .../GetCitiesPageQueryAuthorizer.cs | 31 + .../GetCitiesPageQueryHandler.cs | 55 + .../GetCitiesPageQueryValidator.cs | 43 + .../Cities/Queries/GetCity/GetCityQuery.cs | 8 + .../Queries/GetCity/GetCityQueryAuthorizer.cs | 31 + .../Queries/GetCity/GetCityQueryHandler.cs | 39 + .../Queries/GetCity/GetCityQueryValidator.cs | 14 + .../GetCitiesPageFilterViewModel.cs | 8 + .../Repositories/CityRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 4 + .../Persistence/Configuration.cs | 9 +- src/HttpApi/Controllers/CitiesController.cs | 176 ++ src/HttpApi/Controllers/RegionsController.cs | 6 - .../Configurations/BaseConfiguration.cs | 0 .../Configurations/CountryConfiguration.cs | 0 .../Configurations/RegionConfiguration.cs | 0 src/Persistence/InMemory/InMemoryDbContext.cs | 12 +- .../InMemory/InMemoryUnitOfWork.cs | 4 + .../Repositories/InMemoryCityRepository.cs | 11 + .../Configurations/AddressConfiguration.cs | 55 - .../Configurations/CityConfiguration.cs | 8 - .../RouteAddressConfiguration.cs | 83 - .../Configurations/RouteConfiguration.cs | 30 - ...3338_Countries_Regions_Cities.Designer.cs} | 141 +- ...0250430113338_Countries_Regions_Cities.cs} | 166 +- .../PostgreSqlDbContextModelSnapshot.cs | 137 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 4 + .../Repositories/PostgreSqlCityRepository.cs | 11 + .../CitiesTests.cs | 1576 +++++++++++++++++ .../RegionsTests.cs | 8 +- 44 files changed, 2544 insertions(+), 535 deletions(-) create mode 100644 src/Application/Cities/CityDto.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommand.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQuery.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs create mode 100644 src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs create mode 100644 src/HttpApi/Controllers/CitiesController.cs rename src/Persistence/{InMemory => }/Configurations/BaseConfiguration.cs (100%) rename src/Persistence/{InMemory => }/Configurations/CountryConfiguration.cs (100%) rename src/Persistence/{InMemory => }/Configurations/RegionConfiguration.cs (100%) create mode 100644 src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs rename src/Persistence/PostgreSql/Migrations/{20250427160059_Initial_migration.Designer.cs => 20250430113338_Countries_Regions_Cities.Designer.cs} (65%) rename src/Persistence/PostgreSql/Migrations/{20250427160059_Initial_migration.cs => 20250430113338_Countries_Regions_Cities.cs} (60%) create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs create mode 100644 tst/Application.IntegrationTests/CitiesTests.cs diff --git a/src/Application/Cities/CityDto.cs b/src/Application/Cities/CityDto.cs new file mode 100644 index 0000000..f3a47ff --- /dev/null +++ b/src/Application/Cities/CityDto.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Cities; + +public sealed class CityDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Region.Name)); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs new file mode 100644 index 0000000..3530aec --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public record AddCityCommand : IRequest +{ + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs new file mode 100644 index 0000000..1122f5f --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs new file mode 100644 index 0000000..fa93931 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs @@ -0,0 +1,61 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Name == request.Name && e.Region.Guid == request.RegionUuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "City with given name already exists."); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionUuid, e => e.Country, + cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionUuid} not found."); + } + + entity = new City() + { + Name = request.Name, + RegionId = parentEntity.Id + }; + + entity = await _unitOfWork.CityRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs new file mode 100644 index 0000000..e764345 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandValidator : AbstractValidator +{ + public AddCityCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.RegionUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs new file mode 100644 index 0000000..a8ee4d0 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public record DeleteCityCommand : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs new file mode 100644 index 0000000..ed62a53 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs new file mode 100644 index 0000000..26dc751 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCityCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CityRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs new file mode 100644 index 0000000..292d274 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandValidator : AbstractValidator +{ + public DeleteCityCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs new file mode 100644 index 0000000..992cfc1 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public record UpdateCityCommand : IRequest +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs new file mode 100644 index 0000000..ed1e198 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs new file mode 100644 index 0000000..67a788f --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, e => e.Region.Country, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionUuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionUuid} not found."); + } + + entity.Name = request.Name; + entity.RegionId = parentEntity.Id; + + entity = await _unitOfWork.CityRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs new file mode 100644 index 0000000..c70d946 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandValidator : AbstractValidator +{ + public UpdateCityCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.RegionUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs new file mode 100644 index 0000000..23e2c7d --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public record GetCitiesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs new file mode 100644 index 0000000..84872fe --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCitiesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCitiesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs new file mode 100644 index 0000000..91b5daa --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCitiesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCitiesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CityRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RegionUuid != null + ? e.Region.Guid == request.RegionUuid + : true) && + (request.CountryUuid != null + ? e.Region.Country.Guid == request.CountryUuid + : true), + e => e.Region.Country, + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs new file mode 100644 index 0000000..dde8a20 --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryValidator : AbstractValidator +{ + public GetCitiesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQuery.cs b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs new file mode 100644 index 0000000..1c6f9bd --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public record GetCityQuery : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs new file mode 100644 index 0000000..55f927d --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCityQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCityQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs new file mode 100644 index 0000000..954f646 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCityQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCityQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, e => e.Region.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs new file mode 100644 index 0000000..894166b --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryValidator : AbstractValidator +{ + public GetCityQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs new file mode 100644 index 0000000..68ec4d9 --- /dev/null +++ b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class GetCitiesPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs new file mode 100644 index 0000000..beee75b --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface CityRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index b98a962..9c34925 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -5,8 +5,12 @@ namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; public interface UnitOfWork : IDisposable { CountryRepository CountryRepository { get; } + RegionRepository RegionRepository { get; } + CityRepository CityRepository { get; } + int Save(); + Task SaveAsync(CancellationToken cancellationToken); } diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 5aacbf3..1eb83be 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -72,9 +72,12 @@ public static class Configuration $"{configuration.Type} datastore is not supported."); } - // using var serviceProvider = services.BuildServiceProvider(); - // var unitOfWork = serviceProvider.GetService(); - // DbSeeder.Seed(unitOfWork); + if (configuration.Seed) + { + using var serviceProvider = services.BuildServiceProvider(); + var unitOfWork = serviceProvider.GetService(); + DbSeeder.Seed(unitOfWork); + } return services; } diff --git a/src/HttpApi/Controllers/CitiesController.cs b/src/HttpApi/Controllers/CitiesController.cs new file mode 100644 index 0000000..5286fba --- /dev/null +++ b/src/HttpApi/Controllers/CitiesController.cs @@ -0,0 +1,176 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Cities; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("cities")] +public class CitiesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Create a city")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddCityCommand command, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send(command, cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all cities")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetCitiesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CountryUuid = filterQuery.CountryUuid, + RegionUuid = filterQuery.RegionUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get city by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCityQuery() { Uuid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update city")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateCityCommand command, + CancellationToken cancellationToken) + { + command.Uuid = uuid; + return await Mediator.Send(command, cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete city")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteCityCommand() { Uuid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs index 1306916..4b5a72d 100644 --- a/src/HttpApi/Controllers/RegionsController.cs +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -53,9 +53,6 @@ public class RegionsController : ControllerBase [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(PaginatedList))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] @@ -91,9 +88,6 @@ public class RegionsController : ControllerBase [SwaggerOperation("Get region by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] diff --git a/src/Persistence/InMemory/Configurations/BaseConfiguration.cs b/src/Persistence/Configurations/BaseConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/BaseConfiguration.cs rename to src/Persistence/Configurations/BaseConfiguration.cs diff --git a/src/Persistence/InMemory/Configurations/CountryConfiguration.cs b/src/Persistence/Configurations/CountryConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/CountryConfiguration.cs rename to src/Persistence/Configurations/CountryConfiguration.cs diff --git a/src/Persistence/InMemory/Configurations/RegionConfiguration.cs b/src/Persistence/Configurations/RegionConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/RegionConfiguration.cs rename to src/Persistence/Configurations/RegionConfiguration.cs diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 6c74ec5..0c788c7 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,5 +1,6 @@ -using System.Reflection; +// using System.Reflection; // using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; // using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -10,6 +11,10 @@ public class InMemoryDbContext : DbContext public InMemoryDbContext(DbContextOptions options) : base(options) { } + public DbSet Countries { get => Set(); } + public DbSet Regions { get => Set(); } + public DbSet Cities { get => Set(); } + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -18,11 +23,6 @@ public class InMemoryDbContext : DbContext // "vehicle_type", // VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); // - builder - .ApplyConfigurationsFromAssembly( - Assembly.GetExecutingAssembly(), - t => t.Namespace == - "cuqmbr.TravelGuide.Persistence.InMemory.Configurations"); } protected override void ConfigureConventions( diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 23af267..974c1df 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -15,11 +15,15 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CountryRepository = new InMemoryCountryRepository(_dbContext); RegionRepository = new InMemoryRegionRepository(_dbContext); + CityRepository = new InMemoryCityRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs new file mode 100644 index 0000000..22f942a --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryCityRepository : + InMemoryBaseRepository, CityRepository +{ + public InMemoryCityRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs deleted file mode 100644 index 2efda70..0000000 --- a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class AddressConfiguration : BaseConfiguration
-{ - public override void Configure(EntityTypeBuilder
builder) - { - builder - .ToTable("addresses"); - - base.Configure(builder); - - - builder - .Property(a => a.Name) - .HasColumnName("name") - .HasColumnType("varchar(128)") - .IsRequired(true); - - // builder - // .Property(a => a.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); - - - builder - .Property(a => a.CityId) - .HasColumnName("city_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(a => a.City) - .WithMany(c => c.Addresses) - .HasForeignKey(a => a.CityId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(a => a.CityId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") - .IsUnique(); - } -} diff --git a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs index dc6114c..589d652 100644 --- a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs @@ -36,13 +36,5 @@ public class CityConfiguration : BaseConfiguration $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(c => c.RegionId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") - .IsUnique(); } } diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs deleted file mode 100644 index 547d410..0000000 --- a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs +++ /dev/null @@ -1,83 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class RouteAddressConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("route_addresses"); - - base.Configure(builder); - - - builder - .Property(ra => ra.Order) - .HasColumnName("order") - .HasColumnType("smallint") - .IsRequired(true); - - - builder - .Property(ra => ra.AddressId) - .HasColumnName("address_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(ra => ra.Address) - .WithMany(a => a.AddressRoutes) - .HasForeignKey(ra => ra.AddressId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(ra => ra.AddressId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .Property(ra => ra.RouteId) - .HasColumnName("route_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(ra => ra.Route) - .WithMany(a => a.RouteAddresses) - .HasForeignKey(ra => ra.RouteId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(ra => ra.RouteId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .HasAlternateKey(ra => new { ra.AddressId, ra.RouteId, ra.Order }) - .HasName( - "altk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}_" + - $"{builder.Property(ra => ra.Order).Metadata.GetColumnName()}"); - } -} diff --git a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs deleted file mode 100644 index 6c81f2e..0000000 --- a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class RouteConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("routes"); - - base.Configure(builder); - - - builder - .Property(r => r.Name) - .HasColumnName("name") - .HasColumnType("varchar(64)") - .IsRequired(true); - - // builder - // .Property(r => r.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); - } -} diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs similarity index 65% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs rename to src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs index d68963c..d7efad4 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs @@ -12,8 +12,8 @@ using cuqmbr.TravelGuide.Persistence.PostgreSql; namespace Persistence.PostgreSql.Migrations { [DbContext(typeof(PostgreSqlDbContext))] - [Migration("20250427160059_Initial_migration")] - partial class Initial_migration + [Migration("20250430113338_Countries_Regions_Cities")] + partial class Countries_Regions_Cities { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -27,65 +27,35 @@ namespace Persistence.PostgreSql.Migrations NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.HasSequence("addresses_id_sequence"); - modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); - modelBuilder.HasSequence("route_addresses_id_sequence"); - - modelBuilder.HasSequence("routes_id_sequence"); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CityId") - .HasColumnType("bigint") - .HasColumnName("city_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(128)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_addresses"); + b.HasIndex("CityId"); - b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); - - b.HasIndex("CityId") - .IsUnique() - .HasDatabaseName("ix_addresses_city_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - - b.ToTable("addresses", "application"); + b.ToTable("Address", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -125,9 +95,7 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId") - .IsUnique() - .HasDatabaseName("ix_cities_region_id"); + b.HasIndex("RegionId"); b.ToTable("cities", "application"); }); @@ -214,95 +182,49 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(64)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_routes"); - - b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); - - b.ToTable("routes", "application"); + b.ToTable("Route", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AddressId") - .HasColumnType("bigint") - .HasColumnName("address_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Order") - .HasColumnType("smallint") - .HasColumnName("order"); + .HasColumnType("smallint"); b.Property("RouteId") - .HasColumnType("bigint") - .HasColumnName("route_id"); + .HasColumnType("bigint"); - b.HasKey("Id") - .HasName("pk_route_addresses"); + b.HasKey("Id"); - b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + b.HasIndex("AddressId"); - b.HasAlternateKey("AddressId", "RouteId", "Order") - .HasName("altk_route_addresses_address_id_route_id_order"); + b.HasIndex("RouteId"); - b.HasIndex("AddressId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_address_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - - b.HasIndex("RouteId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_route_id"); - - b.ToTable("route_addresses", "application"); + b.ToTable("RouteAddress", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => @@ -311,8 +233,7 @@ namespace Persistence.PostgreSql.Migrations .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_addresses_city_id"); + .IsRequired(); b.Navigation("City"); }); @@ -347,15 +268,13 @@ namespace Persistence.PostgreSql.Migrations .WithMany("AddressRoutes") .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_address_id"); + .IsRequired(); b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") .WithMany("RouteAddresses") .HasForeignKey("RouteId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_route_id"); + .IsRequired(); b.Navigation("Address"); diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs similarity index 60% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs rename to src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs index 2e209c1..04b8098 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs @@ -1,12 +1,13 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Persistence.PostgreSql.Migrations { /// - public partial class Initial_migration : Migration + public partial class Countries_Regions_Cities : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -17,10 +18,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.AlterDatabase() .Annotation("Npgsql:Enum:vehicle_type", "bus,train,aircraft"); - migrationBuilder.CreateSequence( - name: "addresses_id_sequence", - schema: "application"); - migrationBuilder.CreateSequence( name: "cities_id_sequence", schema: "application"); @@ -33,14 +30,6 @@ namespace Persistence.PostgreSql.Migrations name: "regions_id_sequence", schema: "application"); - migrationBuilder.CreateSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.CreateSequence( - name: "routes_id_sequence", - schema: "application"); - migrationBuilder.CreateTable( name: "countries", schema: "application", @@ -57,19 +46,18 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "routes", + name: "Route", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), - name = table.Column(type: "varchar(64)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_routes", x => x.id); - table.UniqueConstraint("altk_routes_Guid", x => x.uuid); + table.PrimaryKey("PK_Route", x => x.Id); }); migrationBuilder.CreateTable( @@ -119,23 +107,22 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "addresses", + name: "Address", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.addresses_id_sequence')"), - name = table.Column(type: "varchar(128)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), - city_id = table.Column(type: "bigint", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + CityId = table.Column(type: "bigint", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_addresses", x => x.id); - table.UniqueConstraint("altk_addresses_Guid", x => x.uuid); + table.PrimaryKey("PK_Address", x => x.Id); table.ForeignKey( - name: "fk_addresses_city_id", - column: x => x.city_id, + name: "FK_Address_cities_CityId", + column: x => x.CityId, principalSchema: "application", principalTable: "cities", principalColumn: "id", @@ -143,57 +130,41 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "route_addresses", + name: "RouteAddress", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), - order = table.Column(type: "smallint", nullable: false), - address_id = table.Column(type: "bigint", nullable: false), - route_id = table.Column(type: "bigint", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Order = table.Column(type: "smallint", nullable: false), + AddressId = table.Column(type: "bigint", nullable: false), + RouteId = table.Column(type: "bigint", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_route_addresses", x => x.id); - table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); - table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); + table.PrimaryKey("PK_RouteAddress", x => x.Id); table.ForeignKey( - name: "fk_route_addresses_address_id", - column: x => x.address_id, + name: "FK_RouteAddress_Address_AddressId", + column: x => x.AddressId, principalSchema: "application", - principalTable: "addresses", - principalColumn: "id", + principalTable: "Address", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "fk_route_addresses_route_id", - column: x => x.route_id, + name: "FK_RouteAddress_Route_RouteId", + column: x => x.RouteId, principalSchema: "application", - principalTable: "routes", - principalColumn: "id", + principalTable: "Route", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( - name: "ix_addresses_city_id", + name: "IX_Address_CityId", schema: "application", - table: "addresses", - column: "city_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_addresses_id", - schema: "application", - table: "addresses", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_addresses_uuid", - schema: "application", - table: "addresses", - column: "uuid", - unique: true); + table: "Address", + column: "CityId"); migrationBuilder.CreateIndex( name: "ix_cities_id", @@ -203,11 +174,10 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "ix_cities_region_id", + name: "IX_cities_region_id", schema: "application", table: "cities", - column: "region_id", - unique: true); + column: "region_id"); migrationBuilder.CreateIndex( name: "ix_cities_uuid", @@ -251,61 +221,31 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "ix_route_addresses_address_id", + name: "IX_RouteAddress_AddressId", schema: "application", - table: "route_addresses", - column: "address_id", - unique: true); + table: "RouteAddress", + column: "AddressId"); migrationBuilder.CreateIndex( - name: "ix_route_addresses_id", + name: "IX_RouteAddress_RouteId", schema: "application", - table: "route_addresses", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_route_id", - schema: "application", - table: "route_addresses", - column: "route_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_uuid", - schema: "application", - table: "route_addresses", - column: "uuid", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_id", - schema: "application", - table: "routes", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_uuid", - schema: "application", - table: "routes", - column: "uuid", - unique: true); + table: "RouteAddress", + column: "RouteId"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "route_addresses", + name: "RouteAddress", schema: "application"); migrationBuilder.DropTable( - name: "addresses", + name: "Address", schema: "application"); migrationBuilder.DropTable( - name: "routes", + name: "Route", schema: "application"); migrationBuilder.DropTable( @@ -320,10 +260,6 @@ namespace Persistence.PostgreSql.Migrations name: "countries", schema: "application"); - migrationBuilder.DropSequence( - name: "addresses_id_sequence", - schema: "application"); - migrationBuilder.DropSequence( name: "cities_id_sequence", schema: "application"); @@ -335,14 +271,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.DropSequence( name: "regions_id_sequence", schema: "application"); - - migrationBuilder.DropSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.DropSequence( - name: "routes_id_sequence", - schema: "application"); } } } diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 9e89c0f..1d282f6 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -24,65 +24,35 @@ namespace Persistence.PostgreSql.Migrations NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.HasSequence("addresses_id_sequence"); - modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); - modelBuilder.HasSequence("route_addresses_id_sequence"); - - modelBuilder.HasSequence("routes_id_sequence"); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CityId") - .HasColumnType("bigint") - .HasColumnName("city_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(128)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_addresses"); + b.HasIndex("CityId"); - b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); - - b.HasIndex("CityId") - .IsUnique() - .HasDatabaseName("ix_addresses_city_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - - b.ToTable("addresses", "application"); + b.ToTable("Address", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -122,9 +92,7 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId") - .IsUnique() - .HasDatabaseName("ix_cities_region_id"); + b.HasIndex("RegionId"); b.ToTable("cities", "application"); }); @@ -211,95 +179,49 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(64)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_routes"); - - b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); - - b.ToTable("routes", "application"); + b.ToTable("Route", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AddressId") - .HasColumnType("bigint") - .HasColumnName("address_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Order") - .HasColumnType("smallint") - .HasColumnName("order"); + .HasColumnType("smallint"); b.Property("RouteId") - .HasColumnType("bigint") - .HasColumnName("route_id"); + .HasColumnType("bigint"); - b.HasKey("Id") - .HasName("pk_route_addresses"); + b.HasKey("Id"); - b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + b.HasIndex("AddressId"); - b.HasAlternateKey("AddressId", "RouteId", "Order") - .HasName("altk_route_addresses_address_id_route_id_order"); + b.HasIndex("RouteId"); - b.HasIndex("AddressId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_address_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - - b.HasIndex("RouteId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_route_id"); - - b.ToTable("route_addresses", "application"); + b.ToTable("RouteAddress", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => @@ -308,8 +230,7 @@ namespace Persistence.PostgreSql.Migrations .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_addresses_city_id"); + .IsRequired(); b.Navigation("City"); }); @@ -344,15 +265,13 @@ namespace Persistence.PostgreSql.Migrations .WithMany("AddressRoutes") .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_address_id"); + .IsRequired(); b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") .WithMany("RouteAddresses") .HasForeignKey("RouteId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_route_id"); + .IsRequired(); b.Navigation("Address"); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 320b7f2..ae0977a 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -15,11 +15,15 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CountryRepository = new PostgreSqlCountryRepository(_dbContext); RegionRepository = new PostgreSqlRegionRepository(_dbContext); + CityRepository = new PostgreSqlCityRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs new file mode 100644 index 0000000..441d6b9 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlCityRepository : + PostgreSqlBaseRepository, CityRepository +{ + public PostgreSqlCityRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs new file mode 100644 index 0000000..3554879 --- /dev/null +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -0,0 +1,1576 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public class CitiesTests : TestBase +{ + [Fact] + public async Task AddCity_WithAdminRole_CityAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Fact] + public async Task + AddDuplicateCity_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + AddSameCitiesToDifferentRegions_WithAdminRole_CitiesAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult1 = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult1.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult1); + + Assert.NotNull(getCityResult1.Name); + Assert.Equal(cityName, getCityResult1.Name); + + Assert.NotNull(getCityResult1.CountryName); + Assert.Equal(countryName, getCityResult1.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult1.CountryUuid); + + Assert.NotNull(getCityResult1.RegionName); + Assert.Equal(regionName1, getCityResult1.RegionName); + Assert.Equal(addRegionResult1.Uuid, getCityResult1.RegionUuid); + + var getCityResult2 = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult2.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult2); + + Assert.NotNull(getCityResult2.Name); + Assert.Equal(cityName, getCityResult2.Name); + + Assert.NotNull(getCityResult2.CountryName); + Assert.Equal(countryName, getCityResult2.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult2.CountryUuid); + + Assert.NotNull(getCityResult2.RegionName); + Assert.Equal(regionName2, getCityResult2.RegionName); + Assert.Equal(addRegionResult2.Uuid, getCityResult2.RegionUuid); + } + + [Fact] + public async Task + AddCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + AddCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + AddCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task AddCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task UpdateCity_WithAdminRole_CityUpdated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + string newName = "Different Name"; + + var updateCityResult = await mediator.Send( + new UpdateCityCommand() + { + Uuid = addCityResult.Uuid, + Name = newName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(updateCityResult); + + Assert.NotNull(updateCityResult.Name); + Assert.Equal(newName, updateCityResult.Name); + Assert.Equal(addCityResult.Uuid, updateCityResult.Uuid); + + Assert.NotNull(updateCityResult.CountryName); + Assert.Equal(countryName, updateCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, updateCityResult.CountryUuid); + + Assert.NotNull(updateCityResult.RegionName); + Assert.Equal(regionName, updateCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, updateCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + UpdateCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Name = name, + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.NewGuid(), + Name = "Name", + RegionUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Name = "Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.NewGuid(), + Name = "Different Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = addCityResult.Uuid, + Name = "Different Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task UpdateCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task DeleteCity_WithAdminRole_CityDeleted() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await mediator.Send( + new DeleteCityCommand() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + DeleteCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + DeleteCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task DeleteCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCity_WithAdminRole_CityReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCitiesPage_WithAdminRole_CitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name 1"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.True(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(2, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.True(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCountryName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cOuNtRy nAme 1" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByRegionName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "reGioN nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCityName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cItY nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSort_WithAdminRole_SortedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-countryName" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByCountryUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByRegionUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetCitiesPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetCitiesPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetCitiesPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCitiesPage_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetCitiesPage_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) +} diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 16c42eb..d9ff392 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -28,7 +28,7 @@ public class RegionsTests : TestBase Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult = await mediator.Send( new AddRegionCommand() @@ -65,7 +65,7 @@ public class RegionsTests : TestBase Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult = await mediator.Send( new AddRegionCommand() @@ -106,7 +106,7 @@ public class RegionsTests : TestBase Name = countryName2 }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult1 = await mediator.Send( new AddRegionCommand() @@ -517,8 +517,6 @@ public class RegionsTests : TestBase // TODO: Add more tests with unauthenticated user // (copy tests with admin role) - // TODO: Add test for GetRegion and GetRegionPage - [Fact] public async Task GetRegion_WithAdminRole_RegionReturned() { From e70c807b7c2b62e3292d46295d90b3db865f48af Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Apr 2025 17:44:29 +0300 Subject: [PATCH 04/35] update Region DTO to include country name --- src/Application/Regions/RegionDto.cs | 7 +- .../RegionsTests.cs | 312 ++++++++++-------- 2 files changed, 188 insertions(+), 131 deletions(-) diff --git a/src/Application/Regions/RegionDto.cs b/src/Application/Regions/RegionDto.cs index c4ce975..66e8b87 100644 --- a/src/Application/Regions/RegionDto.cs +++ b/src/Application/Regions/RegionDto.cs @@ -11,6 +11,8 @@ public sealed class RegionDto : IMapFrom public Guid CountryUuid { get; set; } + public string CountryName { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() @@ -19,6 +21,9 @@ public sealed class RegionDto : IMapFrom opt => opt.MapFrom(s => s.Guid)) .ForMember( d => d.CountryUuid, - opt => opt.MapFrom(s => s.Country.Guid)); + opt => opt.MapFrom(s => s.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Country.Name)); } } diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index d9ff392..60b7f10 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -14,7 +14,7 @@ namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class RegionsTests : TestBase { [Fact] - public async Task AddRegion_WithAdminRole_RegionCreated() + public async Task AddRegion_WithAdminRole_RegionAdded() { SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); @@ -22,7 +22,7 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName @@ -30,23 +30,25 @@ public class RegionsTests : TestBase string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); Assert.NotNull(getRegionResult.Name); Assert.Equal(regionName, getRegionResult.Name); - Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); } [Fact] @@ -59,7 +61,7 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName @@ -67,24 +69,24 @@ public class RegionsTests : TestBase string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } [Fact] public async Task - AddSameRegionsToDifferentCountries_WithAdminRole_RegionsCreated() + AddSameRegionsToDifferentCountries_WithAdminRole_RegionsAdded() { SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); @@ -92,7 +94,7 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 @@ -100,7 +102,7 @@ public class RegionsTests : TestBase string countryName2 = "Country Name2 "; - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 @@ -108,41 +110,43 @@ public class RegionsTests : TestBase string regionName = "Region Name"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionResult1 = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult1.Uuid, + Uuid = addRegionResult1.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult1); Assert.NotNull(getRegionResult1.Name); Assert.Equal(regionName, getRegionResult1.Name); - Assert.Equal(createCountryResult1.Uuid, getRegionResult1.CountryUuid); + Assert.Equal(addCountryResult1.Uuid, getRegionResult1.CountryUuid); var getRegionResult2 = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult2.Uuid, + Uuid = addRegionResult2.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult2); Assert.NotNull(getRegionResult2.Name); Assert.Equal(regionName, getRegionResult2.Name); - Assert.Equal(createCountryResult2.Uuid, getRegionResult2.CountryUuid); + Assert.Equal(addCountryResult2.Uuid, getRegionResult2.CountryUuid); + Assert.NotNull(getRegionResult2.CountryName); + Assert.Equal(addCountryResult2.Name, getRegionResult2.CountryName); } [Fact] @@ -239,7 +243,7 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName @@ -247,33 +251,35 @@ public class RegionsTests : TestBase string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string newName = "Different Name"; - var editRegionResult = await mediator.Send( + var updateRegionResult = await mediator.Send( new UpdateRegionCommand() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, Name = newName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); Assert.NotNull(getRegionResult.Name); Assert.Equal(newName, getRegionResult.Name); - Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); } [Theory] @@ -344,7 +350,7 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" @@ -355,7 +361,7 @@ public class RegionsTests : TestBase { Uuid = Guid.NewGuid(), Name = "Different Name", - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -367,23 +373,23 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" }, TestContext.Current.CancellationToken); - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = "Name", - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = createCountryResult.Uuid, + Uuid = addCountryResult.Uuid, Name = "Different Name", CountryUuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); @@ -427,29 +433,29 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" }, TestContext.Current.CancellationToken); - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = "Name", - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await mediator.Send( new DeleteRegionCommand() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken)); } @@ -526,7 +532,7 @@ public class RegionsTests : TestBase string countryName = "Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName @@ -534,24 +540,26 @@ public class RegionsTests : TestBase string regionName = "Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryUuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Uuid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); Assert.NotNull(getRegionResult.Name); Assert.Equal(regionName, getRegionResult.Name); - Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); - Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.Equal(addRegionResult.Uuid, getRegionResult.Uuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); } [Theory] @@ -630,13 +638,13 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; string countryName2 = "CountryName 1"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 }, TestContext.Current.CancellationToken); - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 @@ -645,18 +653,18 @@ public class RegionsTests : TestBase string regionName1 = "Region Name 1"; string regionName2 = "Region Name 2"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName1, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -676,11 +684,11 @@ public class RegionsTests : TestBase Assert.Single(getRegionsResult.Items); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Name, getRegionsResult.Items.First().Name); + addRegionResult1.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); getRegionsResult = await mediator.Send( new GetRegionsPageQuery() @@ -699,11 +707,14 @@ public class RegionsTests : TestBase Assert.Single(getRegionsResult.Items); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Name, getRegionsResult.Items.First().Name); + addRegionResult2.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); getRegionsResult = await mediator.Send( new GetRegionsPageQuery() @@ -721,23 +732,29 @@ public class RegionsTests : TestBase Assert.Equal(2, getRegionsResult.Items.Count()); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Name, getRegionsResult.Items.First().Name); + addRegionResult1.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); Assert.NotNull(getRegionsResult.Items.Last().Name); Assert.Equal( - createRegionResult2.Name, getRegionsResult.Items.Last().Name); + addRegionResult2.Name, getRegionsResult.Items.Last().Name); Assert.Equal( - createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); + addRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + addCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.Last().CountryName); } [Fact] public async Task - GetRegionsPage_WithSearch_WithAdminRole_SearchedRegionsPageReturned() + GetRegionsPage_WithSearchByCountryName_WithAdminRole_SearchedRegionsPageReturned() { SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); @@ -746,13 +763,13 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; string countryName2 = "CountryName 1"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 }, TestContext.Current.CancellationToken); - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 @@ -761,18 +778,18 @@ public class RegionsTests : TestBase string regionName1 = "Region Name 1"; string regionName2 = "Region Name 2"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName1, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -793,13 +810,57 @@ public class RegionsTests : TestBase Assert.Single(getRegionsResult.Items); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Name, getRegionsResult.Items.First().Name); + addRegionResult2.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + } - getRegionsResult = await mediator.Send( + [Fact] + public async Task + GetRegionsPage_WithSearchByRegionName_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( new GetRegionsPageQuery() { PageNumber = 1, @@ -817,11 +878,14 @@ public class RegionsTests : TestBase Assert.Single(getRegionsResult.Items); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Name, getRegionsResult.Items.First().Name); + addRegionResult1.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); } [Fact] @@ -835,13 +899,13 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; string countryName2 = "Country Name 2"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 }, TestContext.Current.CancellationToken); - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 @@ -850,18 +914,18 @@ public class RegionsTests : TestBase string regionName1 = "Region Name 1"; string regionName2 = "Region Name 2"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName1, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -869,7 +933,7 @@ public class RegionsTests : TestBase { PageNumber = 1, PageSize = 10, - Sort = "-name" + Sort = "-countryName" }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionsResult); @@ -882,35 +946,14 @@ public class RegionsTests : TestBase Assert.Equal(2, getRegionsResult.Items.Count()); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Name, getRegionsResult.Items.First().Name); + addRegionResult2.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); - - getRegionsResult = await mediator.Send( - new GetRegionsPageQuery() - { - PageNumber = 1, - PageSize = 10, - Sort = "+name" - }, TestContext.Current.CancellationToken); - - Assert.NotNull(getRegionsResult); - Assert.Equal(1, getRegionsResult.PageNumber); - Assert.Equal(2, getRegionsResult.TotalCount); - Assert.Equal(1, getRegionsResult.TotalPages); - Assert.False(getRegionsResult.HasNextPage); - Assert.False(getRegionsResult.HasPreviousPage); - Assert.NotNull(getRegionsResult.Items); - Assert.Equal(2, getRegionsResult.Items.Count()); - Assert.NotNull(getRegionsResult.Items.First().Name); + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); Assert.Equal( - createRegionResult1.Name, getRegionsResult.Items.First().Name); - Assert.Equal( - createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - Assert.Equal( - createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); } [Fact] @@ -924,13 +967,13 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; string countryName2 = "Country Name 2"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 }, TestContext.Current.CancellationToken); - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 @@ -940,25 +983,25 @@ public class RegionsTests : TestBase string regionName2 = "Region Name 2"; string regionName3 = "Region Name 3"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName1, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult3 = await mediator.Send( + var addRegionResult3 = await mediator.Send( new AddRegionCommand() { Name = regionName3, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -966,7 +1009,7 @@ public class RegionsTests : TestBase { PageNumber = 1, PageSize = 10, - CountryUuid = createCountryResult1.Uuid + CountryUuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionsResult); @@ -979,18 +1022,21 @@ public class RegionsTests : TestBase Assert.Single(getRegionsResult.Items); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Name, getRegionsResult.Items.First().Name); + addRegionResult1.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); getRegionsResult = await mediator.Send( new GetRegionsPageQuery() { PageNumber = 1, PageSize = 10, - CountryUuid = createCountryResult2.Uuid + CountryUuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionsResult); @@ -1003,18 +1049,24 @@ public class RegionsTests : TestBase Assert.Equal(2, getRegionsResult.Items.Count()); Assert.NotNull(getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Name, getRegionsResult.Items.First().Name); + addRegionResult2.Name, getRegionsResult.Items.First().Name); Assert.Equal( - createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); Assert.NotNull(getRegionsResult.Items.Last().Name); Assert.Equal( - createRegionResult3.Name, getRegionsResult.Items.Last().Name); + addRegionResult3.Name, getRegionsResult.Items.Last().Name); Assert.Equal( - createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); + addRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); Assert.Equal( - createCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + addCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + Assert.NotNull(getRegionsResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); } [Theory] From d500d1f84c7b040877e78c5e652931c0f72b2fc8 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 1 May 2025 11:48:43 +0300 Subject: [PATCH 05/35] add address entity management --- src/Application/Addresses/AddressDto.cs | 58 ++++++ .../Commands/AddAddress/AddAddressCommand.cs | 17 ++ .../AddAddress/AddAddressCommandAuthorizer.cs | 31 +++ .../AddAddress/AddAddressCommandHandler.cs | 64 ++++++ .../AddAddress/AddAddressCommandValidator.cs | 65 ++++++ .../DeleteAddress/DeleteAddressCommand.cs | 8 + .../DeleteAddressCommandAuthorizer.cs | 31 +++ .../DeleteAddressCommandHandler.cs | 34 +++ .../DeleteAddressCommandValidator.cs | 14 ++ .../UpdateAddress/UpdateAddressCommand.cs | 19 ++ .../UpdateAddressCommandAuthorizer.cs | 31 +++ .../UpdateAddressCommandHandler.cs | 58 ++++++ .../UpdateAddressCommandValidator.cs | 31 +++ .../Queries/GetAddress/GetAddressQuery.cs | 8 + .../GetAddress/GetAddressQueryAuthorizer.cs | 31 +++ .../GetAddress/GetAddressQueryHandler.cs | 39 ++++ .../GetAddress/GetAddressQueryValidator.cs | 14 ++ .../GetAddressesPage/GetAddressesPageQuery.cs | 21 ++ .../GetAddressesPageQueryAuthorizer.cs | 31 +++ .../GetAddressesPageQueryHandler.cs | 59 ++++++ .../GetAddressesPageQueryValidator.cs | 43 ++++ .../ViewModels/AddAddressViewModel.cs | 14 ++ .../GetAddressesPageFilterViewModel.cs | 10 + .../ViewModels/UpdateAddressViewModel.cs | 14 ++ .../Cities/Commands/AddCity/AddCityCommand.cs | 2 +- .../Commands/AddCity/AddCityCommandHandler.cs | 6 +- .../AddCity/AddCityCommandValidator.cs | 2 +- .../Commands/DeleteCity/DeleteCityCommand.cs | 2 +- .../DeleteCity/DeleteCityCommandHandler.cs | 2 +- .../DeleteCity/DeleteCityCommandValidator.cs | 2 +- .../Commands/UpdateCity/UpdateCityCommand.cs | 4 +- .../UpdateCity/UpdateCityCommandHandler.cs | 6 +- .../UpdateCity/UpdateCityCommandValidator.cs | 4 +- .../GetCitiesPage/GetCitiesPageQuery.cs | 4 +- .../GetCitiesPageQueryHandler.cs | 8 +- .../Cities/Queries/GetCity/GetCityQuery.cs | 2 +- .../Queries/GetCity/GetCityQueryHandler.cs | 2 +- .../Queries/GetCity/GetCityQueryValidator.cs | 2 +- .../Cities/ViewModels/AddCityViewModel.cs | 8 + .../Cities/ViewModels/UpdateCityViewModel.cs | 8 + .../Repositories/AddressRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../ViewModels/AddCountryViewModel.cs | 6 + .../ViewModels/UpdateCountryViewModel.cs | 6 + .../Commands/AddRegion/AddRegionCommand.cs | 2 +- .../AddRegion/AddRegionCommandHandler.cs | 6 +- .../AddRegion/AddRegionCommandValidator.cs | 2 +- .../DeleteRegion/DeleteRegionCommand.cs | 2 +- .../DeleteRegionCommandHandler.cs | 2 +- .../DeleteRegionCommandValidator.cs | 2 +- .../UpdateRegion/UpdateRegionCommand.cs | 4 +- .../UpdateRegionCommandHandler.cs | 6 +- .../UpdateRegionCommandValidator.cs | 4 +- .../Queries/GetRegion/GetRegionQuery.cs | 2 +- .../GetRegion/GetRegionQueryHandler.cs | 2 +- .../GetRegion/GetRegionQueryValidator.cs | 2 +- .../GetRegionsPage/GetRegionsPageQuery.cs | 2 +- .../GetRegionsPageQueryHandler.cs | 4 +- .../Regions/ViewModels/AddRegionViewModel.cs | 8 + .../ViewModels/UpdateRegionViewModel.cs | 8 + .../Resources/Localization/en-US.json | 3 +- src/Configuration/Identity/Configuration.cs | 2 +- src/Domain/Entities/Address.cs | 11 +- src/Domain/Enums/VehicleType.cs | 32 +-- .../Controllers/AddressesController.cs | 196 ++++++++++++++++++ src/HttpApi/Controllers/CitiesController.cs | 38 ++-- .../Controllers/CountriesController.cs | 29 ++- src/HttpApi/Controllers/RegionsController.cs | 36 ++-- src/HttpApi/appsettings.Development.json | 2 +- src/Identity/ConfigurationOptions.cs | 2 +- .../Configurations/BaseConfiguration.cs | 55 ----- .../Configurations/CountryConfiguration.cs | 31 --- .../Configurations/RegionConfiguration.cs | 40 ---- src/Persistence/InMemory/InMemoryDbContext.cs | 1 + .../InMemory/InMemoryUnitOfWork.cs | 2 + .../Repositories/InMemoryAddressRepository.cs | 11 + .../Configurations/AddressConfiguration.cs | 64 ++++++ .../Configurations/CityConfiguration.cs | 7 + .../Configurations/RegionConfiguration.cs | 7 + ...ries_Regions_Cities_Addresses.Designer.cs} | 142 +++++-------- ...Add_Countries_Regions_Cities_Addresses.cs} | 122 ++++------- .../PostgreSqlDbContextModelSnapshot.cs | 138 +++++------- .../PostgreSql/PostgreSqlDbContext.cs | 5 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 3 + .../PostgreSqlAddressRepository.cs | 11 + .../CitiesTests.cs | 142 ++++++------- .../RegionsTests.cs | 92 ++++---- 87 files changed, 1494 insertions(+), 617 deletions(-) create mode 100644 src/Application/Addresses/AddressDto.cs create mode 100644 src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs create mode 100644 src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs create mode 100644 src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs create mode 100644 src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs create mode 100644 src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs create mode 100644 src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs create mode 100644 src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs create mode 100644 src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs create mode 100644 src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs create mode 100644 src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs create mode 100644 src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs create mode 100644 src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs create mode 100644 src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs create mode 100644 src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs create mode 100644 src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs create mode 100644 src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs create mode 100644 src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs create mode 100644 src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs create mode 100644 src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs create mode 100644 src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs create mode 100644 src/Application/Addresses/ViewModels/AddAddressViewModel.cs create mode 100644 src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs create mode 100644 src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs create mode 100644 src/Application/Cities/ViewModels/AddCityViewModel.cs create mode 100644 src/Application/Cities/ViewModels/UpdateCityViewModel.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs create mode 100644 src/Application/Countries/ViewModels/AddCountryViewModel.cs create mode 100644 src/Application/Countries/ViewModels/UpdateCountryViewModel.cs create mode 100644 src/Application/Regions/ViewModels/AddRegionViewModel.cs create mode 100644 src/Application/Regions/ViewModels/UpdateRegionViewModel.cs create mode 100644 src/HttpApi/Controllers/AddressesController.cs delete mode 100644 src/Persistence/Configurations/BaseConfiguration.cs delete mode 100644 src/Persistence/Configurations/CountryConfiguration.cs delete mode 100644 src/Persistence/Configurations/RegionConfiguration.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs rename src/Persistence/PostgreSql/Migrations/{20250430113338_Countries_Regions_Cities.Designer.cs => 20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs} (69%) rename src/Persistence/PostgreSql/Migrations/{20250430113338_Countries_Regions_Cities.cs => 20250430180231_Add_Countries_Regions_Cities_Addresses.cs} (65%) create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs diff --git a/src/Application/Addresses/AddressDto.cs b/src/Application/Addresses/AddressDto.cs new file mode 100644 index 0000000..5d936bf --- /dev/null +++ b/src/Application/Addresses/AddressDto.cs @@ -0,0 +1,58 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Addresses; + +public sealed class AddressDto : IMapFrom
+{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.City.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.City.Region.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.City.Name)); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs new file mode 100644 index 0000000..2b1a2f5 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs @@ -0,0 +1,17 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public record AddAddressCommand : IRequest +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } + + public Guid CityGuid { get; set; } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs new file mode 100644 index 0000000..eb8ae61 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs new file mode 100644 index 0000000..5aaf4f9 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs @@ -0,0 +1,64 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddAddressCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Name == request.Name && e.City.Guid == request.CityGuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Address with given name already exists."); + } + + var parentEntity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.CityGuid, e => e.Region.Country, + cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CityGuid} not found."); + } + + entity = new Address() + { + Name = request.Name, + Longitude = request.Longitude, + Latitude = request.Latitude, + VehicleType = request.VehicleType, + CityId = parentEntity.Id + }; + + entity = await _unitOfWork.AddressRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs new file mode 100644 index 0000000..2ca5840 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs @@ -0,0 +1,65 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandValidator : AbstractValidator +{ + public AddAddressCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Latitude) + .GreaterThanOrEqualTo(-90) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + -90)) + .LessThanOrEqualTo(90) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 90)); + + RuleFor(v => v.Longitude) + .GreaterThanOrEqualTo(-180) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + -180)) + .LessThanOrEqualTo(180) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 180)); + + RuleFor(v => v.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.CityGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs new file mode 100644 index 0000000..87241a4 --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public record DeleteAddressCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs new file mode 100644 index 0000000..e8e04ca --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs new file mode 100644 index 0000000..428b7fa --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAddressCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AddressRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs new file mode 100644 index 0000000..10cec3c --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandValidator : AbstractValidator +{ + public DeleteAddressCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs new file mode 100644 index 0000000..b44f8cf --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs @@ -0,0 +1,19 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public record UpdateAddressCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } + + public Guid CityGuid { get; set; } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs new file mode 100644 index 0000000..1063a38 --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs new file mode 100644 index 0000000..eba7089 --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs @@ -0,0 +1,58 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateAddressCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.City.Region.Country, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.CityGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CityGuid} not found."); + } + + entity.Name = request.Name; + entity.Longitude = request.Longitude; + entity.Latitude = request.Latitude; + entity.VehicleType = request.VehicleType; + entity.CityId = parentEntity.Id; + + entity = await _unitOfWork.AddressRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs new file mode 100644 index 0000000..59770c9 --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandValidator : AbstractValidator +{ + public UpdateAddressCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.CityGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs new file mode 100644 index 0000000..fa665c4 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public record GetAddressQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs new file mode 100644 index 0000000..545dd25 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAddressQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAddressQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs new file mode 100644 index 0000000..f0f5a86 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAddressQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAddressQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.City.Region.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs new file mode 100644 index 0000000..5e97a2e --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryValidator : AbstractValidator +{ + public GetAddressQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs new file mode 100644 index 0000000..deaae5a --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public record GetAddressesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CountryGuid { get; set; } + + public Guid? RegionGuid { get; set; } + + public Guid? CityGuid { get; set; } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs new file mode 100644 index 0000000..b8d21da --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAddressesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAddressesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs new file mode 100644 index 0000000..0be0bca --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs @@ -0,0 +1,59 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAddressesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAddressesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AddressRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Region.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.CityGuid != null + ? e.City.Guid == request.CityGuid + : true) && + (request.RegionGuid != null + ? e.City.Region.Guid == request.RegionGuid + : true) && + (request.CountryGuid != null + ? e.City.Region.Country.Guid == request.CountryGuid + : true), + e => e.City.Region.Country, + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs new file mode 100644 index 0000000..af43f25 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryValidator : AbstractValidator +{ + public GetAddressesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Addresses/ViewModels/AddAddressViewModel.cs b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs new file mode 100644 index 0000000..904431c --- /dev/null +++ b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class AddAddressViewModel +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CityUuid { get; set; } +} diff --git a/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs new file mode 100644 index 0000000..610f6c9 --- /dev/null +++ b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class GetAddressesPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } + + public Guid? CityUuid { get; set; } +} diff --git a/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs new file mode 100644 index 0000000..6bcb4e4 --- /dev/null +++ b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class UpdateAddressViewModel +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CityUuid { get; set; } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs index 3530aec..72152a3 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs @@ -6,5 +6,5 @@ public record AddCityCommand : IRequest { public string Name { get; set; } - public Guid RegionUuid { get; set; } + public Guid RegionGuid { get; set; } } diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs index fa93931..51584b4 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs @@ -25,7 +25,7 @@ public class AddCityCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.CityRepository.GetOneAsync( - e => e.Name == request.Name && e.Region.Guid == request.RegionUuid, + e => e.Name == request.Name && e.Region.Guid == request.RegionGuid, cancellationToken); if (entity != null) @@ -35,13 +35,13 @@ public class AddCityCommandHandler : } var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.RegionUuid, e => e.Country, + e => e.Guid == request.RegionGuid, e => e.Country, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.RegionUuid} not found."); + $"Parent entity with Guid: {request.RegionGuid} not found."); } entity = new City() diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs index e764345..2a0a931 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -20,7 +20,7 @@ public class AddCityCommandValidator : AbstractValidator localizer["FluentValidation.MaximumLength"], 64)); - RuleFor(v => v.RegionUuid) + RuleFor(v => v.RegionGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs index a8ee4d0..94dbf43 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; public record DeleteCityCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs index 26dc751..700334d 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs @@ -18,7 +18,7 @@ public class DeleteCityCommandHandler : IRequestHandler CancellationToken cancellationToken) { var entity = await _unitOfWork.CityRepository.GetOneAsync( - e => e.Guid == request.Uuid, cancellationToken); + e => e.Guid == request.Guid, cancellationToken); if (entity == null) { diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs index 292d274..582aa0b 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs @@ -7,7 +7,7 @@ public class DeleteCityCommandValidator : AbstractValidator { public DeleteCityCommandValidator(IStringLocalizer localizer) { - RuleFor(v => v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs index 992cfc1..acafa27 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs @@ -4,9 +4,9 @@ namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; public record UpdateCityCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } public string Name { get; set; } - public Guid RegionUuid { get; set; } + public Guid RegionGuid { get; set; } } diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs index 67a788f..5fecbc5 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs @@ -24,7 +24,7 @@ public class UpdateCityCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.CityRepository.GetOneAsync( - e => e.Guid == request.Uuid, e => e.Region.Country, + e => e.Guid == request.Guid, e => e.Region.Country, cancellationToken); if (entity == null) @@ -33,12 +33,12 @@ public class UpdateCityCommandHandler : } var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.RegionUuid, cancellationToken); + e => e.Guid == request.RegionGuid, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.RegionUuid} not found."); + $"Parent entity with Guid: {request.RegionGuid} not found."); } entity.Name = request.Name; diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs index c70d946..b813ca2 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -10,7 +10,7 @@ public class UpdateCityCommandValidator : AbstractValidator IStringLocalizer localizer, CultureService cultureService) { - RuleFor(v => v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); @@ -24,7 +24,7 @@ public class UpdateCityCommandValidator : AbstractValidator localizer["FluentValidation.MaximumLength"], 64)); - RuleFor(v => v.RegionUuid) + RuleFor(v => v.RegionGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs index 23e2c7d..2fdcac5 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs @@ -13,7 +13,7 @@ public record GetCitiesPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public Guid? CountryUuid { get; set; } + public Guid? CountryGuid { get; set; } - public Guid? RegionUuid { get; set; } + public Guid? RegionGuid { get; set; } } diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs index 91b5daa..3d4d568 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs @@ -29,11 +29,11 @@ public class GetCitiesPageQueryHandler : (e.Name.ToLower().Contains(request.Search.ToLower()) || e.Region.Name.ToLower().Contains(request.Search.ToLower()) || e.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && - (request.RegionUuid != null - ? e.Region.Guid == request.RegionUuid + (request.RegionGuid != null + ? e.Region.Guid == request.RegionGuid : true) && - (request.CountryUuid != null - ? e.Region.Country.Guid == request.CountryUuid + (request.CountryGuid != null + ? e.Region.Country.Guid == request.CountryGuid : true), e => e.Region.Country, request.PageNumber, request.PageSize, diff --git a/src/Application/Cities/Queries/GetCity/GetCityQuery.cs b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs index 1c6f9bd..4f2c898 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQuery.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; public record GetCityQuery : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs index 954f646..68e3aec 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs @@ -24,7 +24,7 @@ public class GetCityQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.CityRepository.GetOneAsync( - e => e.Guid == request.Uuid, e => e.Region.Country, + e => e.Guid == request.Guid, e => e.Region.Country, cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs index 894166b..1c7fbb5 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs @@ -7,7 +7,7 @@ public class GetCityQueryValidator : AbstractValidator { public GetCityQueryValidator(IStringLocalizer localizer) { - RuleFor(v => v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Cities/ViewModels/AddCityViewModel.cs b/src/Application/Cities/ViewModels/AddCityViewModel.cs new file mode 100644 index 0000000..ad60681 --- /dev/null +++ b/src/Application/Cities/ViewModels/AddCityViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class AddCityViewModel +{ + public string Name { get; set; } + + public Guid RegionGuid { get; set; } +} diff --git a/src/Application/Cities/ViewModels/UpdateCityViewModel.cs b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs new file mode 100644 index 0000000..05f7e11 --- /dev/null +++ b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class UpdateCityViewModel +{ + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs new file mode 100644 index 0000000..3dfcbf4 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface AddressRepository : BaseRepository
{ } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 9c34925..78d0a07 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -10,6 +10,8 @@ public interface UnitOfWork : IDisposable CityRepository CityRepository { get; } + AddressRepository AddressRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Countries/ViewModels/AddCountryViewModel.cs b/src/Application/Countries/ViewModels/AddCountryViewModel.cs new file mode 100644 index 0000000..f244738 --- /dev/null +++ b/src/Application/Countries/ViewModels/AddCountryViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Countries.ViewModels; + +public sealed class AddCountryViewModel +{ + public string Name { get; set; } +} diff --git a/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs b/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs new file mode 100644 index 0000000..20fe7f5 --- /dev/null +++ b/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Countries.ViewModels; + +public sealed class UpdateCountryViewModel +{ + public string Name { get; set; } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs index cc9c48f..e3369a0 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs @@ -6,5 +6,5 @@ public record AddRegionCommand : IRequest { public string Name { get; set; } - public Guid CountryUuid { get; set; } + public Guid CountryGuid { get; set; } } diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs index 87357bd..0b5000e 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs @@ -25,7 +25,7 @@ public class AddRegionCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Name == request.Name && e.Country.Guid == request.CountryUuid, + e => e.Name == request.Name && e.Country.Guid == request.CountryGuid, cancellationToken); if (entity != null) @@ -35,12 +35,12 @@ public class AddRegionCommandHandler : } var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( - e => e.Guid == request.CountryUuid, cancellationToken); + e => e.Guid == request.CountryGuid, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.CountryUuid} not found."); + $"Parent entity with Guid: {request.CountryGuid} not found."); } entity = new Region() diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs index 6cbc97d..c35227d 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs @@ -20,7 +20,7 @@ public class AddRegionCommandValidator : AbstractValidator localizer["FluentValidation.MaximumLength"], 64)); - RuleFor(v => v.CountryUuid) + RuleFor(v => v.CountryGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs index b6839cd..2e43800 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; public record DeleteRegionCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs index b52384b..aba2cf7 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs @@ -18,7 +18,7 @@ public class DeleteRegionCommandHandler : IRequestHandler CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, cancellationToken); + e => e.Guid == request.Guid, cancellationToken); if (entity == null) { diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs index 5bfaf0b..1b62b86 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs @@ -7,7 +7,7 @@ public class DeleteRegionCommandValidator : AbstractValidator v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs index 1ff6fb2..2bbe7c3 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs @@ -4,9 +4,9 @@ namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; public record UpdateRegionCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } public string Name { get; set; } - public Guid CountryUuid { get; set; } + public Guid CountryGuid { get; set; } } diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs index f5fd163..5f8d461 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs @@ -24,7 +24,7 @@ public class UpdateRegionCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, cancellationToken); + e => e.Guid == request.Guid, cancellationToken); if (entity == null) { @@ -32,12 +32,12 @@ public class UpdateRegionCommandHandler : } var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( - e => e.Guid == request.CountryUuid, cancellationToken); + e => e.Guid == request.CountryGuid, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.CountryUuid} not found."); + $"Parent entity with Guid: {request.CountryGuid} not found."); } entity.Name = request.Name; diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs index a259cd5..6cb3dfb 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs @@ -10,7 +10,7 @@ public class UpdateRegionCommandValidator : AbstractValidator v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); @@ -24,7 +24,7 @@ public class UpdateRegionCommandValidator : AbstractValidator v.CountryUuid) + RuleFor(v => v.CountryGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs index 32b5f5d..eb0431e 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; public record GetRegionQuery : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs index 1af903e..c50a2df 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs @@ -24,7 +24,7 @@ public class GetRegionQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, e => e.Country, + e => e.Guid == request.Guid, e => e.Country, cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs index 90e7f09..e6fc08d 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs @@ -7,7 +7,7 @@ public class GetRegionQueryValidator : AbstractValidator { public GetRegionQueryValidator(IStringLocalizer localizer) { - RuleFor(v => v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs index d618dd6..9d8ad93 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs @@ -13,5 +13,5 @@ public record GetRegionsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public Guid? CountryUuid { get; set; } + public Guid? CountryGuid { get; set; } } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs index 1e40663..42c1143 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs @@ -28,8 +28,8 @@ public class GetRegionsPageQueryHandler : e => (e.Name.ToLower().Contains(request.Search.ToLower()) || e.Country.Name.ToLower().Contains(request.Search.ToLower())) && - (request.CountryUuid != null - ? e.Country.Guid == request.CountryUuid + (request.CountryGuid != null + ? e.Country.Guid == request.CountryGuid : true), e => e.Country, request.PageNumber, request.PageSize, diff --git a/src/Application/Regions/ViewModels/AddRegionViewModel.cs b/src/Application/Regions/ViewModels/AddRegionViewModel.cs new file mode 100644 index 0000000..4b1273c --- /dev/null +++ b/src/Application/Regions/ViewModels/AddRegionViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Regions.ViewModels; + +public sealed class AddRegionViewModel +{ + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs b/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs new file mode 100644 index 0000000..e3025df --- /dev/null +++ b/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Regions.ViewModels; + +public sealed class UpdateRegionViewModel +{ + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 481cb0f..cec7742 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -3,7 +3,8 @@ "MaximumLength": "Must less than {0:G} characters.", "NotEmpty": "Must not be empty.", "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", - "LessThanOrEqualTo": "Must be less than or equal to {0:G}." + "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", + "MustBeInEnum": "Must be one of the following: {0}." }, "ExceptionHandling": { "ValidationException": { diff --git a/src/Configuration/Identity/Configuration.cs b/src/Configuration/Identity/Configuration.cs index d9c1d2a..d066380 100644 --- a/src/Configuration/Identity/Configuration.cs +++ b/src/Configuration/Identity/Configuration.cs @@ -45,7 +45,7 @@ public static class Configuration .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - if (configuration.Datastore.Initialize) + if (configuration.Datastore.Migrate) { using var dbContextServiceProvider = services.BuildServiceProvider(); PostgreSqlInitializer.Initialize(dbContextServiceProvider); diff --git a/src/Domain/Entities/Address.cs b/src/Domain/Entities/Address.cs index 81e017b..1c00718 100644 --- a/src/Domain/Entities/Address.cs +++ b/src/Domain/Entities/Address.cs @@ -7,11 +7,11 @@ public sealed class Address : EntityBase public string Name { get; set; } // TODO: Implement coordinates using NetTopologySuite - // public double Longitude { get; set; } - // - // public double Latitude { get; set; } + public double Longitude { get; set; } - // public VehicleType VehicleType { get; set; } + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } public long CityId { get; set; } @@ -19,6 +19,5 @@ public sealed class Address : EntityBase public City City { get; set; } - public ICollection AddressRoutes { get; set; } + // public ICollection AddressRoutes { get; set; } } - diff --git a/src/Domain/Enums/VehicleType.cs b/src/Domain/Enums/VehicleType.cs index a107370..30402d1 100644 --- a/src/Domain/Enums/VehicleType.cs +++ b/src/Domain/Enums/VehicleType.cs @@ -5,24 +5,24 @@ namespace cuqmbr.TravelGuide.Domain.Enums; public abstract class VehicleType : Enumeration { - public static readonly VehicleType Bus = new BusVehicleType(); - public static readonly VehicleType Train = new TrainVehicleType(); - public static readonly VehicleType Aircraft = new AircraftVehicleType(); + public static readonly VehicleType Bus = new BusVehicleType(); + public static readonly VehicleType Train = new TrainVehicleType(); + public static readonly VehicleType Aircraft = new AircraftVehicleType(); - protected VehicleType(int value, string name) : base(value, name) { } + protected VehicleType(int value, string name) : base(value, name) { } - private sealed class BusVehicleType : VehicleType - { - public BusVehicleType() : base(0, "bus") { } - } + private sealed class BusVehicleType : VehicleType + { + public BusVehicleType() : base(0, "bus") { } + } - private sealed class TrainVehicleType : VehicleType - { - public TrainVehicleType() : base(1, "train") { } - } + private sealed class TrainVehicleType : VehicleType + { + public TrainVehicleType() : base(1, "train") { } + } - private sealed class AircraftVehicleType : VehicleType - { - public AircraftVehicleType() : base(2, "aircraft") { } - } + private sealed class AircraftVehicleType : VehicleType + { + public AircraftVehicleType() : base(2, "aircraft") { } + } } diff --git a/src/HttpApi/Controllers/AddressesController.cs b/src/HttpApi/Controllers/AddressesController.cs new file mode 100644 index 0000000..4d34676 --- /dev/null +++ b/src/HttpApi/Controllers/AddressesController.cs @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Addresses; +using cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; +using cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; +using cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; +using cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; +using cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; +using cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("addresses")] +public class AddressesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add an address")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddAddressViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAddressCommand() + { + Name = viewModel.Name, + Longitude = viewModel.Longitude, + Latitude = viewModel.Latitude, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + CityGuid = viewModel.CityUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all addresses")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAddressesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAddressesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CountryGuid = filterQuery.CountryUuid, + RegionGuid = filterQuery.RegionUuid, + CityGuid = filterQuery.CityUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get an address by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAddressQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update an address")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateAddressViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAddressCommand() + { + Guid = uuid, + Name = viewModel.Name, + Longitude = viewModel.Longitude, + Latitude = viewModel.Latitude, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + CityGuid = viewModel.CityUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete an address")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AddressDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAddressCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/CitiesController.cs b/src/HttpApi/Controllers/CitiesController.cs index 5286fba..086a5ad 100644 --- a/src/HttpApi/Controllers/CitiesController.cs +++ b/src/HttpApi/Controllers/CitiesController.cs @@ -16,7 +16,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class CitiesController : ControllerBase { [HttpPost] - [SwaggerOperation("Create a city")] + [SwaggerOperation("Add a city")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(CityDto))] @@ -40,12 +40,18 @@ public class CitiesController : ControllerBase StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] public async Task> Add( - [FromBody] AddCityCommand command, + [FromBody] AddCityViewModel viewModel, CancellationToken cancellationToken) { return StatusCode( StatusCodes.Status201Created, - await Mediator.Send(command, cancellationToken)); + await Mediator.Send( + new AddCityCommand() + { + Name = viewModel.Name, + RegionGuid = viewModel.RegionGuid + }, + cancellationToken)); } [HttpGet] @@ -79,14 +85,14 @@ public class CitiesController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CountryUuid = filterQuery.CountryUuid, - RegionUuid = filterQuery.RegionUuid + CountryGuid = filterQuery.CountryUuid, + RegionGuid = filterQuery.RegionUuid }, cancellationToken); } [HttpGet("{uuid:guid}")] - [SwaggerOperation("Get city by uuid")] + [SwaggerOperation("Get a city by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CityDto))] [SwaggerResponse( @@ -108,12 +114,12 @@ public class CitiesController : ControllerBase [FromRoute] Guid uuid, CancellationToken cancellationToken) { - return await Mediator.Send(new GetCityQuery() { Uuid = uuid }, + return await Mediator.Send(new GetCityQuery() { Guid = uuid }, cancellationToken); } [HttpPut("{uuid:guid}")] - [SwaggerOperation("Update city")] + [SwaggerOperation("Update a city")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CityDto))] [SwaggerResponse( @@ -139,15 +145,21 @@ public class CitiesController : ControllerBase typeof(ProblemDetails))] public async Task Update( [FromRoute] Guid uuid, - [FromBody] UpdateCityCommand command, + [FromBody] UpdateCityViewModel viewModel, CancellationToken cancellationToken) { - command.Uuid = uuid; - return await Mediator.Send(command, cancellationToken); + return await Mediator.Send( + new UpdateCityCommand() + { + Guid = uuid, + Name= viewModel.Name, + RegionGuid = viewModel.RegionUuid + }, + cancellationToken); } [HttpDelete("{uuid:guid}")] - [SwaggerOperation("Delete city")] + [SwaggerOperation("Delete a city")] [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", @@ -169,7 +181,7 @@ public class CitiesController : ControllerBase CancellationToken cancellationToken) { await Mediator.Send( - new DeleteCityCommand() { Uuid = uuid }, + new DeleteCityCommand() { Guid = uuid }, cancellationToken); return StatusCode(StatusCodes.Status204NoContent); } diff --git a/src/HttpApi/Controllers/CountriesController.cs b/src/HttpApi/Controllers/CountriesController.cs index d40fa01..76dae58 100644 --- a/src/HttpApi/Controllers/CountriesController.cs +++ b/src/HttpApi/Controllers/CountriesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Countries.ViewModels; using cuqmbr.TravelGuide.Application.Countries; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; @@ -15,7 +16,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class CountriesController : ControllerBase { [HttpPost] - [SwaggerOperation("Create a country")] + [SwaggerOperation("Add a country")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(CountryDto))] @@ -36,12 +37,17 @@ public class CountriesController : ControllerBase StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] public async Task> Add( - [FromBody] AddCountryCommand command, + [FromBody] AddCountryViewModel viewModel, CancellationToken cancellationToken) { return StatusCode( StatusCodes.Status201Created, - await Mediator.Send(command, cancellationToken)); + await Mediator.Send( + new AddCountryCommand() + { + Name = viewModel.Name + }, + cancellationToken)); } [HttpGet] @@ -78,7 +84,7 @@ public class CountriesController : ControllerBase } [HttpGet("{uuid:guid}")] - [SwaggerOperation("Get country by uuid")] + [SwaggerOperation("Get a country by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] [SwaggerResponse( @@ -105,7 +111,7 @@ public class CountriesController : ControllerBase } [HttpPut("{uuid:guid}")] - [SwaggerOperation("Update country")] + [SwaggerOperation("Update a country")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] [SwaggerResponse( @@ -128,15 +134,20 @@ public class CountriesController : ControllerBase typeof(ProblemDetails))] public async Task Update( [FromRoute] Guid uuid, - [FromBody] UpdateCountryCommand command, + [FromBody] UpdateCountryViewModel viewModel, CancellationToken cancellationToken) { - command.Guid = uuid; - return await Mediator.Send(command, cancellationToken); + return await Mediator.Send( + new UpdateCountryCommand() + { + Guid = uuid, + Name = viewModel.Name + }, + cancellationToken); } [HttpDelete("{uuid:guid}")] - [SwaggerOperation("Delete country")] + [SwaggerOperation("Delete a country")] [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs index 4b5a72d..478ec03 100644 --- a/src/HttpApi/Controllers/RegionsController.cs +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Regions.ViewModels; using cuqmbr.TravelGuide.Application.Regions; using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; @@ -16,7 +17,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class RegionsController : ControllerBase { [HttpPost] - [SwaggerOperation("Create a region")] + [SwaggerOperation("Add a region")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(RegionDto))] @@ -40,12 +41,17 @@ public class RegionsController : ControllerBase StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] public async Task> Add( - [FromBody] AddRegionCommand command, + [FromBody] AddRegionViewModel viewModel, CancellationToken cancellationToken) { return StatusCode( StatusCodes.Status201Created, - await Mediator.Send(command, cancellationToken)); + await Mediator.Send( + new AddRegionCommand() + { + Name = viewModel.Name + }, + cancellationToken)); } [HttpGet] @@ -79,13 +85,13 @@ public class RegionsController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CountryUuid = filterQuery.CountryUuid + CountryGuid = filterQuery.CountryUuid }, cancellationToken); } [HttpGet("{uuid:guid}")] - [SwaggerOperation("Get region by uuid")] + [SwaggerOperation("Get a region by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] [SwaggerResponse( @@ -107,12 +113,12 @@ public class RegionsController : ControllerBase [FromRoute] Guid uuid, CancellationToken cancellationToken) { - return await Mediator.Send(new GetRegionQuery() { Uuid = uuid }, + return await Mediator.Send(new GetRegionQuery() { Guid = uuid }, cancellationToken); } [HttpPut("{uuid:guid}")] - [SwaggerOperation("Update region")] + [SwaggerOperation("Update a region")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] [SwaggerResponse( @@ -138,15 +144,21 @@ public class RegionsController : ControllerBase typeof(ProblemDetails))] public async Task Update( [FromRoute] Guid uuid, - [FromBody] UpdateRegionCommand command, + [FromBody] UpdateRegionViewModel viewModel, CancellationToken cancellationToken) { - command.Uuid = uuid; - return await Mediator.Send(command, cancellationToken); + return await Mediator.Send( + new UpdateRegionCommand() + { + Guid = uuid, + Name = viewModel.Name, + CountryGuid = viewModel.CountryUuid + }, + cancellationToken); } [HttpDelete("{uuid:guid}")] - [SwaggerOperation("Delete region")] + [SwaggerOperation("Delete a region")] [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", @@ -168,7 +180,7 @@ public class RegionsController : ControllerBase CancellationToken cancellationToken) { await Mediator.Send( - new DeleteRegionCommand() { Uuid = uuid }, + new DeleteRegionCommand() { Guid = uuid }, cancellationToken); return StatusCode(StatusCodes.Status204NoContent); } diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 4b2b05b..2bdff62 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -8,7 +8,7 @@ }, "Datastore": { "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" }, "Localization": { "DefaultCultureName": "en-US", diff --git a/src/Identity/ConfigurationOptions.cs b/src/Identity/ConfigurationOptions.cs index bf10582..9bce7be 100644 --- a/src/Identity/ConfigurationOptions.cs +++ b/src/Identity/ConfigurationOptions.cs @@ -17,7 +17,7 @@ public sealed class Datastore public string PartitionName { get; set; } = "identity"; - public bool Initialize { get; set; } = true; + public bool Migrate { get; set; } = true; } public sealed class JsonWebToken diff --git a/src/Persistence/Configurations/BaseConfiguration.cs b/src/Persistence/Configurations/BaseConfiguration.cs deleted file mode 100644 index 4ed923a..0000000 --- a/src/Persistence/Configurations/BaseConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class BaseConfiguration : IEntityTypeConfiguration - where TEntity : EntityBase -{ - public virtual void Configure(EntityTypeBuilder builder) - { - builder - .HasKey(b => b.Id); - // .HasName($"pk_{builder.Metadata.GetTableName()}"); - - builder - .Property(b => b.Id) - .HasColumnName("id"); - // .HasColumnType("bigint") - // .UseSequence( - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + - // "sequence"); - // - // builder - // .HasIndex(b => b.Id) - // .HasDatabaseName( - // "ix_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") - // .IsUnique(); - // - // - // builder - // .HasAlternateKey(b => b.Guid) - // .HasName( - // "altk_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); - // - builder - .Property(b => b.Guid) - .HasColumnName("uuid") - // .HasColumnType("uuid") - .IsRequired(true); - // - // builder - // .HasIndex(b => b.Guid) - // .HasDatabaseName( - // "ix_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") - // .IsUnique(); - } -} diff --git a/src/Persistence/Configurations/CountryConfiguration.cs b/src/Persistence/Configurations/CountryConfiguration.cs deleted file mode 100644 index a335dfa..0000000 --- a/src/Persistence/Configurations/CountryConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class CountryConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("countries"); - - base.Configure(builder); - - - builder - .Property(c => c.Name) - .HasColumnName("name") - // .HasColumnType("varchar(64)") - .IsRequired(true); - - // TODO: Remove comment - // builder - // .HasAlternateKey(c => c.Name) - // .HasName( - // "altk_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(c => c.Name).Metadata.GetColumnName()}"); - } -} diff --git a/src/Persistence/Configurations/RegionConfiguration.cs b/src/Persistence/Configurations/RegionConfiguration.cs deleted file mode 100644 index 87c4f8f..0000000 --- a/src/Persistence/Configurations/RegionConfiguration.cs +++ /dev/null @@ -1,40 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class RegionConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("regions"); - - base.Configure(builder); - - - builder - .Property(r => r.Name) - .HasColumnName("name") - // .HasColumnType("varchar(64)") - .IsRequired(true); - - - builder - .Property(r => r.CountryId) - .HasColumnName("country_id") - // .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(r => r.Country) - .WithMany(c => c.Regions) - .HasForeignKey(r => r.CountryId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - } -} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 0c788c7..fa0cbef 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -14,6 +14,7 @@ public class InMemoryDbContext : DbContext public DbSet Countries { get => Set(); } public DbSet Regions { get => Set(); } public DbSet Cities { get => Set(); } + public DbSet
Addresses { get => Set
(); } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 974c1df..0f10021 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -24,6 +24,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public CityRepository CityRepository { get; init; } + public AddressRepository AddressRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs new file mode 100644 index 0000000..3b206f1 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAddressRepository : + InMemoryBaseRepository
, AddressRepository +{ + public InMemoryAddressRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs new file mode 100644 index 0000000..7d7d53b --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs @@ -0,0 +1,64 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AddressConfiguration : BaseConfiguration
+{ + public override void Configure(EntityTypeBuilder
builder) + { + builder + .Property(a => a.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "addresses", + b => b.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(a => a.Name) + .HasColumnName("name") + .HasColumnType("varchar(128)") + .IsRequired(true); + + + builder + .Property(a => a.CityId) + .HasColumnName("city_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(a => a.City) + .WithMany(c => c.Addresses) + .HasForeignKey(a => a.CityId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(a => a.CityId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs index 589d652..ddafa7e 100644 --- a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs @@ -36,5 +36,12 @@ public class CityConfiguration : BaseConfiguration $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(c => c.RegionId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs index 1cd8573..b1005a5 100644 --- a/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs @@ -36,5 +36,12 @@ public class RegionConfiguration : BaseConfiguration $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(r => r.CountryId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs similarity index 69% rename from src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs rename to src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs index d7efad4..92c0355 100644 --- a/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs @@ -12,8 +12,8 @@ using cuqmbr.TravelGuide.Persistence.PostgreSql; namespace Persistence.PostgreSql.Migrations { [DbContext(typeof(PostgreSqlDbContext))] - [Migration("20250430113338_Countries_Regions_Cities")] - partial class Countries_Regions_Cities + [Migration("20250430180231_Add_Countries_Regions_Cities_Addresses")] + partial class Add_Countries_Regions_Cities_Addresses { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -24,9 +24,10 @@ namespace Persistence.PostgreSql.Migrations .HasAnnotation("ProductVersion", "9.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.HasSequence("addresses_id_sequence"); + modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); @@ -37,25 +38,57 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); b.Property("CityId") - .HasColumnType("bigint"); + .HasColumnType("bigint") + .HasColumnName("city_id"); b.Property("Guid") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); b.Property("Name") .IsRequired() - .HasColumnType("text"); + .HasColumnType("varchar(128)") + .HasColumnName("name"); - b.HasKey("Id"); + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); - b.HasIndex("CityId"); + b.HasKey("Id") + .HasName("pk_addresses"); - b.ToTable("Address", "application"); + b.HasAlternateKey("Guid") + .HasName("altk_addresses_Guid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -95,7 +128,8 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId"); + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); b.ToTable("cities", "application"); }); @@ -165,7 +199,8 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_regions_Guid"); - b.HasIndex("CountryId"); + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); b.HasIndex("Guid") .IsUnique() @@ -178,62 +213,14 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("regions", "application"); }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Guid") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Route", "application"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AddressId") - .HasColumnType("bigint"); - - b.Property("Guid") - .HasColumnType("uuid"); - - b.Property("Order") - .HasColumnType("smallint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("AddressId"); - - b.HasIndex("RouteId"); - - b.ToTable("RouteAddress", "application"); - }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); b.Navigation("City"); }); @@ -262,30 +249,6 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Country"); }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => - { - b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") - .WithMany("AddressRoutes") - .HasForeignKey("AddressId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") - .WithMany("RouteAddresses") - .HasForeignKey("RouteId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Address"); - - b.Navigation("Route"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => - { - b.Navigation("AddressRoutes"); - }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => { b.Navigation("Addresses"); @@ -300,11 +263,6 @@ namespace Persistence.PostgreSql.Migrations { b.Navigation("Cities"); }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => - { - b.Navigation("RouteAddresses"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs similarity index 65% rename from src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs rename to src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs index 04b8098..bc5649f 100644 --- a/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs @@ -1,13 +1,12 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Persistence.PostgreSql.Migrations { /// - public partial class Countries_Regions_Cities : Migration + public partial class Add_Countries_Regions_Cities_Addresses : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -15,8 +14,9 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.EnsureSchema( name: "application"); - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:vehicle_type", "bus,train,aircraft"); + migrationBuilder.CreateSequence( + name: "addresses_id_sequence", + schema: "application"); migrationBuilder.CreateSequence( name: "cities_id_sequence", @@ -45,21 +45,6 @@ namespace Persistence.PostgreSql.Migrations table.UniqueConstraint("altk_countries_Guid", x => x.uuid); }); - migrationBuilder.CreateTable( - name: "Route", - schema: "application", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - Guid = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Route", x => x.Id); - }); - migrationBuilder.CreateTable( name: "regions", schema: "application", @@ -107,64 +92,51 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "Address", + name: "addresses", schema: "application", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - CityId = table.Column(type: "bigint", nullable: false), - Guid = table.Column(type: "uuid", nullable: false) + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.addresses_id_sequence')"), + name = table.Column(type: "varchar(128)", nullable: false), + Longitude = table.Column(type: "double precision", nullable: false), + Latitude = table.Column(type: "double precision", nullable: false), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + city_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_Address", x => x.Id); + table.PrimaryKey("pk_addresses", x => x.id); + table.UniqueConstraint("altk_addresses_Guid", x => x.uuid); + table.CheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); table.ForeignKey( - name: "FK_Address_cities_CityId", - column: x => x.CityId, + name: "fk_addresses_city_id", + column: x => x.city_id, principalSchema: "application", principalTable: "cities", principalColumn: "id", onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "RouteAddress", + migrationBuilder.CreateIndex( + name: "ix_addresses_city_id", schema: "application", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Order = table.Column(type: "smallint", nullable: false), - AddressId = table.Column(type: "bigint", nullable: false), - RouteId = table.Column(type: "bigint", nullable: false), - Guid = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RouteAddress", x => x.Id); - table.ForeignKey( - name: "FK_RouteAddress_Address_AddressId", - column: x => x.AddressId, - principalSchema: "application", - principalTable: "Address", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_RouteAddress_Route_RouteId", - column: x => x.RouteId, - principalSchema: "application", - principalTable: "Route", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + table: "addresses", + column: "city_id"); migrationBuilder.CreateIndex( - name: "IX_Address_CityId", + name: "ix_addresses_id", schema: "application", - table: "Address", - column: "CityId"); + table: "addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid", + unique: true); migrationBuilder.CreateIndex( name: "ix_cities_id", @@ -174,7 +146,7 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "IX_cities_region_id", + name: "ix_cities_region_id", schema: "application", table: "cities", column: "region_id"); @@ -201,7 +173,7 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "IX_regions_country_id", + name: "ix_regions_country_id", schema: "application", table: "regions", column: "country_id"); @@ -219,33 +191,13 @@ namespace Persistence.PostgreSql.Migrations table: "regions", column: "uuid", unique: true); - - migrationBuilder.CreateIndex( - name: "IX_RouteAddress_AddressId", - schema: "application", - table: "RouteAddress", - column: "AddressId"); - - migrationBuilder.CreateIndex( - name: "IX_RouteAddress_RouteId", - schema: "application", - table: "RouteAddress", - column: "RouteId"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "RouteAddress", - schema: "application"); - - migrationBuilder.DropTable( - name: "Address", - schema: "application"); - - migrationBuilder.DropTable( - name: "Route", + name: "addresses", schema: "application"); migrationBuilder.DropTable( @@ -260,6 +212,10 @@ namespace Persistence.PostgreSql.Migrations name: "countries", schema: "application"); + migrationBuilder.DropSequence( + name: "addresses_id_sequence", + schema: "application"); + migrationBuilder.DropSequence( name: "cities_id_sequence", schema: "application"); diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 1d282f6..d8602a8 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -21,9 +21,10 @@ namespace Persistence.PostgreSql.Migrations .HasAnnotation("ProductVersion", "9.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.HasSequence("addresses_id_sequence"); + modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); @@ -34,25 +35,57 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); b.Property("CityId") - .HasColumnType("bigint"); + .HasColumnType("bigint") + .HasColumnName("city_id"); b.Property("Guid") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); b.Property("Name") .IsRequired() - .HasColumnType("text"); + .HasColumnType("varchar(128)") + .HasColumnName("name"); - b.HasKey("Id"); + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); - b.HasIndex("CityId"); + b.HasKey("Id") + .HasName("pk_addresses"); - b.ToTable("Address", "application"); + b.HasAlternateKey("Guid") + .HasName("altk_addresses_Guid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -92,7 +125,8 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId"); + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); b.ToTable("cities", "application"); }); @@ -162,7 +196,8 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_regions_Guid"); - b.HasIndex("CountryId"); + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); b.HasIndex("Guid") .IsUnique() @@ -175,62 +210,14 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("regions", "application"); }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Guid") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Route", "application"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AddressId") - .HasColumnType("bigint"); - - b.Property("Guid") - .HasColumnType("uuid"); - - b.Property("Order") - .HasColumnType("smallint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("AddressId"); - - b.HasIndex("RouteId"); - - b.ToTable("RouteAddress", "application"); - }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); b.Navigation("City"); }); @@ -259,30 +246,6 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Country"); }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => - { - b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") - .WithMany("AddressRoutes") - .HasForeignKey("AddressId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") - .WithMany("RouteAddresses") - .HasForeignKey("RouteId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Address"); - - b.Navigation("Route"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => - { - b.Navigation("AddressRoutes"); - }); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => { b.Navigation("Addresses"); @@ -297,11 +260,6 @@ namespace Persistence.PostgreSql.Migrations { b.Navigation("Cities"); }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => - { - b.Navigation("RouteAddresses"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index fff9242..009b9a6 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -24,15 +24,12 @@ public class PostgreSqlDbContext : DbContext builder.HasDefaultSchema(DefaultSchema); - builder.HasPostgresEnum( - "vehicle_type", - VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); - builder .ApplyConfigurationsFromAssembly( Assembly.GetExecutingAssembly(), t => t.Namespace == "cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations"); + } protected override void ConfigureConventions( diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index ae0977a..bd97758 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -16,6 +16,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CountryRepository = new PostgreSqlCountryRepository(_dbContext); RegionRepository = new PostgreSqlRegionRepository(_dbContext); CityRepository = new PostgreSqlCityRepository(_dbContext); + AddressRepository = new PostgreSqlAddressRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -24,6 +25,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public CityRepository CityRepository { get; init; } + public AddressRepository AddressRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs new file mode 100644 index 0000000..58915b0 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAddressRepository : + PostgreSqlBaseRepository
, AddressRepository +{ + public PostgreSqlAddressRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs index 3554879..4a4c68e 100644 --- a/tst/Application.IntegrationTests/CitiesTests.cs +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -35,7 +35,7 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName = "City Name"; @@ -44,13 +44,13 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); var getCityResult = await mediator.Send( new GetCityQuery() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getCityResult); @@ -89,7 +89,7 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName = "City Name"; @@ -98,14 +98,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -132,14 +132,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName = "City Name"; @@ -148,20 +148,20 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCityResult1 = await mediator.Send( new GetCityQuery() { - Uuid = addCityResult1.Uuid, + Guid = addCityResult1.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getCityResult1); @@ -180,7 +180,7 @@ public class CitiesTests : TestBase var getCityResult2 = await mediator.Send( new GetCityQuery() { - Uuid = addCityResult2.Uuid, + Guid = addCityResult2.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getCityResult2); @@ -209,7 +209,7 @@ public class CitiesTests : TestBase mediator.Send(new AddCityCommand() { Name = "Name", - RegionUuid = Guid.NewGuid() + RegionGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -246,7 +246,7 @@ public class CitiesTests : TestBase mediator.Send(new AddCityCommand() { Name = "Name", - RegionUuid = + RegionGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -303,7 +303,7 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName = "City Name"; @@ -312,7 +312,7 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); string newName = "Different Name"; @@ -320,9 +320,9 @@ public class CitiesTests : TestBase var updateCityResult = await mediator.Send( new UpdateCityCommand() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, Name = newName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(updateCityResult); @@ -355,7 +355,7 @@ public class CitiesTests : TestBase mediator.Send(new UpdateCityCommand() { Name = name, - RegionUuid = Guid.NewGuid() + RegionGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -373,9 +373,9 @@ public class CitiesTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateCityCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Name", - RegionUuid = + RegionGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -394,9 +394,9 @@ public class CitiesTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateCityCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, Name = "Name", - RegionUuid = Guid.NewGuid() + RegionGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -418,15 +418,15 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = "Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new UpdateCityCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Different Name", - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -448,22 +448,22 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = "Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var addCityResult = await mediator.Send( new AddCityCommand() { Name = "Name", - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new UpdateCityCommand() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, Name = "Different Name", - RegionUuid = Guid.NewGuid() + RegionGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -515,26 +515,26 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = "Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var addCityResult = await mediator.Send( new AddCityCommand() { Name = "Name", - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); await mediator.Send( new DeleteCityCommand() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new GetCityQuery() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, }, TestContext.Current.CancellationToken)); } @@ -552,7 +552,7 @@ public class CitiesTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteCityCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -567,7 +567,7 @@ public class CitiesTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteCityCommand() { - Uuid = Guid.NewGuid() + Guid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -623,7 +623,7 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName = "Name"; @@ -632,13 +632,13 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName, - RegionUuid = addRegionResult.Uuid + RegionGuid = addRegionResult.Uuid }, TestContext.Current.CancellationToken); var getCityResult = await mediator.Send( new GetCityQuery() { - Uuid = addCityResult.Uuid, + Guid = addCityResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getCityResult); @@ -670,7 +670,7 @@ public class CitiesTests : TestBase mediator.Send( new GetCityQuery() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -686,7 +686,7 @@ public class CitiesTests : TestBase mediator.Send( new GetCityQuery() { - Uuid = Guid.NewGuid() + Guid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -743,14 +743,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -760,14 +760,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -928,14 +928,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -945,14 +945,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1022,14 +1022,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -1039,14 +1039,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1116,14 +1116,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -1133,14 +1133,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1210,14 +1210,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -1227,14 +1227,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1324,14 +1324,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -1341,14 +1341,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1356,7 +1356,7 @@ public class CitiesTests : TestBase { PageNumber = 1, PageSize = 10, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getCitiesResult); @@ -1419,14 +1419,14 @@ public class CitiesTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); string cityName1 = "City Name 1"; @@ -1436,14 +1436,14 @@ public class CitiesTests : TestBase new AddCityCommand() { Name = cityName1, - RegionUuid = addRegionResult1.Uuid + RegionGuid = addRegionResult1.Uuid }, TestContext.Current.CancellationToken); var addCityResult2 = await mediator.Send( new AddCityCommand() { Name = cityName2, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); var getCitiesResult = await mediator.Send( @@ -1451,7 +1451,7 @@ public class CitiesTests : TestBase { PageNumber = 1, PageSize = 10, - RegionUuid = addRegionResult2.Uuid + RegionGuid = addRegionResult2.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getCitiesResult); diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 60b7f10..78e0991 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -34,13 +34,13 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); @@ -73,14 +73,14 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -114,20 +114,20 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionResult1 = await mediator.Send( new GetRegionQuery() { - Uuid = addRegionResult1.Uuid, + Guid = addRegionResult1.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult1); @@ -138,7 +138,7 @@ public class RegionsTests : TestBase var getRegionResult2 = await mediator.Send( new GetRegionQuery() { - Uuid = addRegionResult2.Uuid, + Guid = addRegionResult2.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult2); @@ -161,7 +161,7 @@ public class RegionsTests : TestBase mediator.Send(new AddRegionCommand() { Name = "Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -198,7 +198,7 @@ public class RegionsTests : TestBase mediator.Send(new AddRegionCommand() { Name = "Name", - CountryUuid = + CountryGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -255,7 +255,7 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string newName = "Different Name"; @@ -263,15 +263,15 @@ public class RegionsTests : TestBase var updateRegionResult = await mediator.Send( new UpdateRegionCommand() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, Name = newName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); @@ -297,7 +297,7 @@ public class RegionsTests : TestBase mediator.Send(new UpdateRegionCommand() { Name = name, - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -315,9 +315,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Name", - CountryUuid = + CountryGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -336,9 +336,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, Name = "Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -359,9 +359,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Different Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -383,15 +383,15 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = "Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = addCountryResult.Uuid, + Guid = addCountryResult.Uuid, Name = "Different Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -443,19 +443,19 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = "Name", - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await mediator.Send( new DeleteRegionCommand() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new GetRegionQuery() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken)); } @@ -473,7 +473,7 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteRegionCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -488,7 +488,7 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteRegionCommand() { - Uuid = Guid.NewGuid() + Guid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -544,13 +544,13 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName, - CountryUuid = addCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = addRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); @@ -577,7 +577,7 @@ public class RegionsTests : TestBase mediator.Send( new GetRegionQuery() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -593,7 +593,7 @@ public class RegionsTests : TestBase mediator.Send( new GetRegionQuery() { - Uuid = Guid.NewGuid() + Guid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -657,14 +657,14 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -782,14 +782,14 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -850,14 +850,14 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -918,14 +918,14 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -987,21 +987,21 @@ public class RegionsTests : TestBase new AddRegionCommand() { Name = regionName1, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName2, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var addRegionResult3 = await mediator.Send( new AddRegionCommand() { Name = regionName3, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionsResult = await mediator.Send( @@ -1009,7 +1009,7 @@ public class RegionsTests : TestBase { PageNumber = 1, PageSize = 10, - CountryUuid = addCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionsResult); @@ -1036,7 +1036,7 @@ public class RegionsTests : TestBase { PageNumber = 1, PageSize = 10, - CountryUuid = addCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionsResult); From fdf147fe8360937db20366ca89ad4949f2dc3fcf Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 1 May 2025 20:50:22 +0300 Subject: [PATCH 06/35] add route entity management --- .../Common/Exceptions/ValidationException.cs | 15 + .../Repositories/RouteRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../Resources/Localization/en-US.json | 4 + .../Commands/AddRoute/AddRouteCommand.cs | 14 + .../AddRoute/AddRouteCommandAuthorizer.cs | 31 ++ .../AddRoute/AddRouteCommandHandler.cs | 84 ++++ .../AddRoute/AddRouteCommandValidator.cs | 45 ++ .../DeleteRoute/DeleteRouteCommand.cs | 8 + .../DeleteRouteCommandAuthorizer.cs | 31 ++ .../DeleteRoute/DeleteRouteCommandHandler.cs | 37 ++ .../DeleteRouteCommandValidator.cs | 14 + .../UpdateRoute/UpdateRouteCommand.cs | 16 + .../UpdateRouteCommandAuthorizer.cs | 31 ++ .../UpdateRoute/UpdateRouteCommandHandler.cs | 108 +++++ .../UpdateRouteCommandValidator.cs | 49 +++ .../Routes/Models/RouteAddressModel.cs | 8 + .../Routes/Queries/GetRoute/GetRouteQuery.cs | 8 + .../GetRoute/GetRouteQueryAuthorizer.cs | 31 ++ .../Queries/GetRoute/GetRouteQueryHandler.cs | 60 +++ .../GetRoute/GetRouteQueryValidator.cs | 14 + .../GetRoutesPage/GetRoutesPageQuery.cs | 18 + .../GetRoutesPageQueryAuthorizer.cs | 31 ++ .../GetRoutesPageQueryHandler.cs | 74 ++++ .../GetRoutesPageQueryValidator.cs | 44 ++ src/Application/Routes/RouteAddressDto.cs | 70 +++ src/Application/Routes/RouteDto.cs | 29 ++ .../Routes/ViewModels/AddRouteViewModel.cs | 10 + .../GetRoutesPageFilterViewModel.cs | 6 + .../ViewModels/RouteAddressViewModel.cs | 8 + .../Routes/ViewModels/UpdateRouteViewModel.cs | 10 + src/Domain/Entities/Address.cs | 2 +- src/Domain/Entities/Route.cs | 3 +- src/HttpApi/Controllers/RoutesController.cs | 198 +++++++++ .../GlobalExceptionHandlerMiddleware.cs | 49 ++- .../InMemory/InMemoryUnitOfWork.cs | 4 + .../Repositories/InMemoryRouteRepository.cs | 11 + .../Configurations/AddressConfiguration.cs | 2 +- .../RouteAddressConfiguration.cs | 81 ++++ .../Configurations/RouteConfiguration.cs | 40 ++ ...6_Add_Route_and_RouteAddresses.Designer.cs | 400 ++++++++++++++++++ ...0501112816_Add_Route_and_RouteAddresses.cs | 132 ++++++ .../PostgreSqlDbContextModelSnapshot.cs | 131 ++++++ .../PostgreSql/PostgreSqlDbContext.cs | 11 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 3 + .../Repositories/PostgreSqlRouteRepository.cs | 11 + .../TypeConverters/VehicleTypeConverter.cs | 13 + 47 files changed, 1978 insertions(+), 29 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs create mode 100644 src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs create mode 100644 src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs create mode 100644 src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs create mode 100644 src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs create mode 100644 src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs create mode 100644 src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs create mode 100644 src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs create mode 100644 src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs create mode 100644 src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs create mode 100644 src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs create mode 100644 src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs create mode 100644 src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs create mode 100644 src/Application/Routes/Models/RouteAddressModel.cs create mode 100644 src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs create mode 100644 src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs create mode 100644 src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs create mode 100644 src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs create mode 100644 src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs create mode 100644 src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs create mode 100644 src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs create mode 100644 src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs create mode 100644 src/Application/Routes/RouteAddressDto.cs create mode 100644 src/Application/Routes/RouteDto.cs create mode 100644 src/Application/Routes/ViewModels/AddRouteViewModel.cs create mode 100644 src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs create mode 100644 src/Application/Routes/ViewModels/RouteAddressViewModel.cs create mode 100644 src/Application/Routes/ViewModels/UpdateRouteViewModel.cs create mode 100644 src/HttpApi/Controllers/RoutesController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs create mode 100644 src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs diff --git a/src/Application/Common/Exceptions/ValidationException.cs b/src/Application/Common/Exceptions/ValidationException.cs index 7c1df67..0ed4b17 100644 --- a/src/Application/Common/Exceptions/ValidationException.cs +++ b/src/Application/Common/Exceptions/ValidationException.cs @@ -10,9 +10,24 @@ public class ValidationException : Exception Errors = new Dictionary(); } + public ValidationException(string message) : base(message) + { + Errors = new Dictionary(); + } + public ValidationException(IEnumerable failures) : this() { + // TODO: Make serialized dictionary look more like this + // "errors": { + // "viewModel": [ + // "The viewModel field is required." + // ], + // "$.addresses[0].order": [ + // "The JSON value could not be converted to System.Int16. Path: $.addresses[0].order | LineNumber: 5 | BytePositionInLine: 26." + // ] + // }, + Errors = failures .GroupBy(f => f.PropertyName, f => f.ErrorMessage) .ToDictionary(fg => fg.Key, fg => fg.ToArray()); diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs new file mode 100644 index 0000000..83249fb --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface RouteRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 78d0a07..fde3ef8 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -12,6 +12,8 @@ public interface UnitOfWork : IDisposable AddressRepository AddressRepository { get; } + RouteRepository RouteRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index cec7742..86a35db 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -6,6 +6,10 @@ "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", "MustBeInEnum": "Must be one of the following: {0}." }, + "Validation": { + "DistinctOrder": "Must have distinct order values.", + "SameVehicleType": "Must have the same vehicle type." + }, "ExceptionHandling": { "ValidationException": { "Title": "One or more validation errors occurred.", diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs new file mode 100644 index 0000000..01180a3 --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public record AddRouteCommand : IRequest +{ + public string Name { get; set; } + + public VehicleType VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs new file mode 100644 index 0000000..9040482 --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs new file mode 100644 index 0000000..df3c7cc --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs @@ -0,0 +1,84 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using FluentValidation.Results; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public AddRouteCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + AddRouteCommand request, + CancellationToken cancellationToken) + { + var page = await _unitOfWork.AddressRepository.GetPageAsync( + e => request.Addresses.Select(a => a.Guid).Contains(e.Guid), + e => e.City.Region.Country, + 1, request.Addresses.Count, cancellationToken); + + var invalidVehicleTypeAddress = + page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType); + if (invalidVehicleTypeAddress != null) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Addresses), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + var pageContainsAllRequestedAddresses = + request.Addresses.Select(e => e.Guid) + .All(e => page.Items.Select(e => e.Guid).Contains(e)); + if (!pageContainsAllRequestedAddresses) + { + var notFoundCount = request.Addresses.Count - page.TotalCount; + throw new NotFoundException( + $"{notFoundCount} addresses was not found."); + } + + var entity = new Route() + { + Name = request.Name, + VehicleType = request.VehicleType, + RouteAddresses = request.Addresses.Select( + e => new RouteAddress() + { + Order = e.Order, + AddressId = page.Items.Single(i => i.Guid == e.Guid).Id + }) + .OrderBy(e => e.Order) + .ToArray() + }; + + entity = await _unitOfWork.RouteRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs new file mode 100644 index 0000000..ead366b --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandValidator : AbstractValidator +{ + public AddRouteCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.Addresses.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.Addresses) + .Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count()) + .WithMessage(localizer["Validation.DistinctOrder"]); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs new file mode 100644 index 0000000..dbf98bc --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public record DeleteRouteCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs new file mode 100644 index 0000000..8978db7 --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs new file mode 100644 index 0000000..d1fde57 --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs @@ -0,0 +1,37 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteRouteCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteRouteCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for Vehicles that using this route in Enrollments + // Delete if there are no such Vehicles + + await _unitOfWork.RouteRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs new file mode 100644 index 0000000..3c655d2 --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandValidator : AbstractValidator +{ + public DeleteRouteCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs new file mode 100644 index 0000000..4fd3797 --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs @@ -0,0 +1,16 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public record UpdateRouteCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public VehicleType VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs new file mode 100644 index 0000000..611b7ae --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs new file mode 100644 index 0000000..df48877 --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs @@ -0,0 +1,108 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + public IStringLocalizer _localizer { get; set; } + + public UpdateRouteCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateRouteCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, + e => e.RouteAddresses, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var page = await _unitOfWork.AddressRepository.GetPageAsync( + e => request.Addresses.Select(a => a.Guid).Contains(e.Guid), + e => e.City.Region.Country, + 1, request.Addresses.Count, cancellationToken); + + var invalidVehicleTypeAddress = + page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType); + if (invalidVehicleTypeAddress != null) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Addresses), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + var pageContainsAllRequestedAddresses = + request.Addresses.Select(e => e.Guid) + .All(e => page.Items.Select(e => e.Guid).Contains(e)); + if (!pageContainsAllRequestedAddresses) + { + var notFoundCount = request.Addresses.Count - page.TotalCount; + throw new NotFoundException( + $"{notFoundCount} addresses was not found."); + } + + + entity.Guid = request.Guid; + entity.Name = request.Name; + entity.VehicleType = request.VehicleType; + + + var requestRouteAddresses = request.Addresses.Select( + e => new RouteAddress() + { + Order = e.Order, + AddressId = page.Items.Single(i => i.Guid == e.Guid).Id + }); + + var commonRouteAddresses = entity.RouteAddresses.IntersectBy( + requestRouteAddresses.Select(ra => (ra.Order, ra.AddressId)), + ra => (ra.Order, ra.AddressId)); + + var newRouteAddresses = requestRouteAddresses.ExceptBy( + entity.RouteAddresses.Select(ra => (ra.Order, ra.AddressId)), + ra => (ra.Order, ra.AddressId)); + + var combinedRouteAddresses = commonRouteAddresses.UnionBy( + newRouteAddresses, ra => (ra.Order, ra.AddressId)); + + entity.RouteAddresses = combinedRouteAddresses + .OrderBy(e => e.Order).ToList(); + + + entity = await _unitOfWork.RouteRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs new file mode 100644 index 0000000..cc4ddd3 --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs @@ -0,0 +1,49 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandValidator : AbstractValidator +{ + public UpdateRouteCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.Addresses.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.Addresses) + .Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count()) + .WithMessage(localizer["Validation.DistinctOrder"]); + } +} diff --git a/src/Application/Routes/Models/RouteAddressModel.cs b/src/Application/Routes/Models/RouteAddressModel.cs new file mode 100644 index 0000000..2bcde99 --- /dev/null +++ b/src/Application/Routes/Models/RouteAddressModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Routes.Models; + +public sealed class RouteAddressModel +{ + public short Order { get; set; } + + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs new file mode 100644 index 0000000..4d8ad72 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public record GetRouteQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs new file mode 100644 index 0000000..7ca5727 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRouteQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRouteQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs new file mode 100644 index 0000000..0f8f2db --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRouteQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetRouteQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddresses, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Find a way to include through lists + var addresses = await _unitOfWork.AddressRepository.GetPageAsync( + e => entity.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id), + e => e.City.Region.Country, + 1, entity.RouteAddresses.Count, cancellationToken); + + entity.RouteAddresses = entity.RouteAddresses.Select( + e => new RouteAddress() + { + Id = e.Id, + Guid = e.Guid, + Order = e.Order, + RouteId = e.RouteId, + Route = e.Route, + AddressId = e.AddressId, + Address = addresses.Items.First(a => a.Id == e.AddressId) + }) + .OrderBy(e => e.Order) + .ToArray(); + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs new file mode 100644 index 0000000..30b065a --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryValidator : AbstractValidator +{ + public GetRouteQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs new file mode 100644 index 0000000..9680329 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs @@ -0,0 +1,18 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public record GetRoutesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public VehicleType? VehicleType { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs new file mode 100644 index 0000000..dc64fa3 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRoutesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRoutesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs new file mode 100644 index 0000000..15e3304 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs @@ -0,0 +1,74 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRoutesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetRoutesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.RouteRepository.GetPageAsync( + e => + e.Name.ToLower().Contains(request.Search.ToLower()) && + (request.VehicleType != null + ? e.VehicleType == request.VehicleType + : true), + e => e.RouteAddresses, + request.PageNumber, request.PageSize, + cancellationToken); + + foreach (var route in paginatedList.Items) + { + // TODO: Find a way to include through lists + var addresses = await _unitOfWork.AddressRepository.GetPageAsync( + e => route.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id), + e => e.City.Region.Country, + 1, route.RouteAddresses.Count, cancellationToken); + + route.RouteAddresses = route.RouteAddresses.Select( + e => new RouteAddress() + { + Id = e.Id, + Guid = e.Guid, + Order = e.Order, + RouteId = e.RouteId, + Route = e.Route, + AddressId = e.AddressId, + Address = addresses.Items.First(a => a.Id == e.AddressId) + }) + .OrderBy(e => e.Order) + .ToArray(); + } + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs new file mode 100644 index 0000000..3ad2ba7 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs @@ -0,0 +1,44 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryValidator : AbstractValidator +{ + public GetRoutesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Routes/RouteAddressDto.cs b/src/Application/Routes/RouteAddressDto.cs new file mode 100644 index 0000000..13288f0 --- /dev/null +++ b/src/Application/Routes/RouteAddressDto.cs @@ -0,0 +1,70 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes; + +public sealed class RouteAddressDto : IMapFrom +{ + public short Order { get; set; } + + + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Address.Guid)) + .ForMember( + d => d.Name, + opt => opt.MapFrom(s => s.Address.Name)) + .ForMember( + d => d.Longitude, + opt => opt.MapFrom(s => s.Address.Longitude)) + .ForMember( + d => d.Latitude, + opt => opt.MapFrom(s => s.Address.Latitude)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.Address.VehicleType.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.Address.City.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Address.City.Region.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Name)); + } +} diff --git a/src/Application/Routes/RouteDto.cs b/src/Application/Routes/RouteDto.cs new file mode 100644 index 0000000..8b97da9 --- /dev/null +++ b/src/Application/Routes/RouteDto.cs @@ -0,0 +1,29 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes; + +public sealed class RouteDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Addresses, + opt => opt.MapFrom(s => s.RouteAddresses)); + } +} diff --git a/src/Application/Routes/ViewModels/AddRouteViewModel.cs b/src/Application/Routes/ViewModels/AddRouteViewModel.cs new file mode 100644 index 0000000..1370a5c --- /dev/null +++ b/src/Application/Routes/ViewModels/AddRouteViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class AddRouteViewModel +{ + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs b/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs new file mode 100644 index 0000000..d3e3a4a --- /dev/null +++ b/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class GetRoutesPageFilterViewModel +{ + public string? VehicleType { get; set; } +} diff --git a/src/Application/Routes/ViewModels/RouteAddressViewModel.cs b/src/Application/Routes/ViewModels/RouteAddressViewModel.cs new file mode 100644 index 0000000..922665d --- /dev/null +++ b/src/Application/Routes/ViewModels/RouteAddressViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class RouteAddressViewModel +{ + public short Order { get; set; } + + public Guid Uuid { get; set; } +} diff --git a/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs b/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs new file mode 100644 index 0000000..5245b17 --- /dev/null +++ b/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class UpdateRouteViewModel +{ + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Domain/Entities/Address.cs b/src/Domain/Entities/Address.cs index 1c00718..5d32156 100644 --- a/src/Domain/Entities/Address.cs +++ b/src/Domain/Entities/Address.cs @@ -19,5 +19,5 @@ public sealed class Address : EntityBase public City City { get; set; } - // public ICollection AddressRoutes { get; set; } + public ICollection AddressRoutes { get; set; } } diff --git a/src/Domain/Entities/Route.cs b/src/Domain/Entities/Route.cs index 8199c35..f947c43 100644 --- a/src/Domain/Entities/Route.cs +++ b/src/Domain/Entities/Route.cs @@ -6,9 +6,8 @@ public sealed class Route : EntityBase { public string Name { get; set; } - // public VehicleType VehicleType { get; set; } + public VehicleType VehicleType { get; set; } public ICollection RouteAddresses { get; set; } } - diff --git a/src/HttpApi/Controllers/RoutesController.cs b/src/HttpApi/Controllers/RoutesController.cs new file mode 100644 index 0000000..1735a76 --- /dev/null +++ b/src/HttpApi/Controllers/RoutesController.cs @@ -0,0 +1,198 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Routes; +using cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; +using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; +using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; +using cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; +using cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; +using cuqmbr.TravelGuide.Application.Routes.ViewModels; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("routes")] +public class RoutesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a route")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(RouteDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddRouteViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddRouteCommand() + { + Name = viewModel.Name, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + Addresses = viewModel.Addresses.Select( + e => new RouteAddressModel() + { + Order = e.Order, + Guid = e.Uuid + + }).ToArray() + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all routes")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetRoutesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetRoutesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + VehicleType = VehicleType.FromName(filterQuery.VehicleType) + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a route by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(RouteDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetRouteQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a route")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateRouteViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateRouteCommand() + { + Guid = uuid, + Name = viewModel.Name, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + Addresses = viewModel.Addresses.Select( + e => new RouteAddressModel() + { + Order = e.Order, + Guid = e.Uuid + + }).ToArray() + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a route")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteRouteCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs index 39c0018..6e303b5 100644 --- a/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs +++ b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -1,7 +1,7 @@ using cuqmbr.TravelGuide.Application.Common.Exceptions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; -using System.Reflection; +using System.Diagnostics; namespace cuqmbr.TravelGuide.HttpApi.Middlewares; @@ -11,6 +11,8 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware private readonly ILogger _logger; private readonly IStringLocalizer _localizer; + + public GlobalExceptionHandlerMiddleware( ILogger logger, IStringLocalizer localizer) @@ -75,7 +77,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails(ex.Errors) + await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetailsWithTraceId(ex.Errors) { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -90,7 +92,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status401Unauthorized; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status401Unauthorized, Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1", @@ -105,7 +107,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -120,7 +122,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -135,7 +137,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -150,7 +152,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status403Forbidden, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3", @@ -165,7 +167,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -180,7 +182,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status404NotFound; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status404NotFound, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", @@ -193,7 +195,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status500InternalServerError; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status500InternalServerError, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", @@ -202,9 +204,26 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware }); } - // class ProblemDetailsWithTraceId : ProblemDetails - // { - // public string TraceId { get; init; } = Activity.Current?.TraceId.ToString(); - // } -} + class ProblemDetailsWithTraceId : ProblemDetails + { + public ProblemDetailsWithTraceId() + { + Extensions = new Dictionary() + { + ["traceId"] = Activity.Current.Id + }; + } + } + class HttpValidationProblemDetailsWithTraceId : HttpValidationProblemDetails + { + public HttpValidationProblemDetailsWithTraceId( + IDictionary errors) : base(errors) + { + Extensions = new Dictionary() + { + ["traceId"] = Activity.Current.Id + }; + } + } +} diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 0f10021..b719aad 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -16,6 +16,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CountryRepository = new InMemoryCountryRepository(_dbContext); RegionRepository = new InMemoryRegionRepository(_dbContext); CityRepository = new InMemoryCityRepository(_dbContext); + AddressRepository = new InMemoryAddressRepository(_dbContext); + RouteRepository = new InMemoryRouteRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -26,6 +28,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public AddressRepository AddressRepository { get; init; } + public RouteRepository RouteRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs new file mode 100644 index 0000000..f728978 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteRepository : + InMemoryBaseRepository, RouteRepository +{ + public InMemoryRouteRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs index 7d7d53b..a7c96f2 100644 --- a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs @@ -18,7 +18,7 @@ public class AddressConfiguration : BaseConfiguration
builder .ToTable( "addresses", - b => b.HasCheckConstraint( + a => a.HasCheckConstraint( "ck_" + $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(a => a.VehicleType) diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs new file mode 100644 index 0000000..9d44a61 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs @@ -0,0 +1,81 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteAddressConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("route_addresses"); + + base.Configure(builder); + + + builder + .Property(ra => ra.Order) + .HasColumnName("order") + .HasColumnType("smallint") + .IsRequired(true); + + + builder + .Property(ra => ra.AddressId) + .HasColumnName("address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ra => ra.Address) + .WithMany(a => a.AddressRoutes) + .HasForeignKey(ra => ra.AddressId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ra => ra.AddressId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}"); + + + builder + .Property(ra => ra.RouteId) + .HasColumnName("route_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ra => ra.Route) + .WithMany(a => a.RouteAddresses) + .HasForeignKey(ra => ra.RouteId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ra => ra.RouteId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}"); + + + builder + .HasAlternateKey(ra => new { ra.AddressId, ra.RouteId, ra.Order }) + .HasName( + "altk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}_" + + $"{builder.Property(ra => ra.Order).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs new file mode 100644 index 0000000..de9177a --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs @@ -0,0 +1,40 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(r => r.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "routes", + r => r.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(r => r.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs new file mode 100644 index 0000000..3e5d1c4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs @@ -0,0 +1,400 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250501112816_Add_Route_and_RouteAddresses")] + partial class Add_Route_and_RouteAddresses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_Guid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_cities_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_cities_id"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_countries_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_countries_id"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_Guid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_regions_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_regions_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_routes_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_routes_id"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_Guid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_route_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_route_addresses_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs new file mode 100644 index 0000000..2908cf2 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs @@ -0,0 +1,132 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Route_and_RouteAddresses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "routes_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "routes", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_routes", x => x.id); + table.UniqueConstraint("altk_routes_Guid", x => x.uuid); + table.CheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + migrationBuilder.CreateTable( + name: "route_addresses", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), + order = table.Column(type: "smallint", nullable: false), + address_id = table.Column(type: "bigint", nullable: false), + route_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_route_addresses", x => x.id); + table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); + table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_route_addresses_address_id", + column: x => x.address_id, + principalSchema: "application", + principalTable: "addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_route_addresses_route_id", + column: x => x.route_id, + principalSchema: "application", + principalTable: "routes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_address_id", + schema: "application", + table: "route_addresses", + column: "address_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_route_id", + schema: "application", + table: "route_addresses", + column: "route_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_id", + schema: "application", + table: "routes", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes", + column: "uuid", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "route_addresses", + schema: "application"); + + migrationBuilder.DropTable( + name: "routes", + schema: "application"); + + migrationBuilder.DropSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "routes_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index d8602a8..f76eb3e 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -31,6 +31,10 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("regions_id_sequence"); + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") @@ -210,6 +214,102 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("regions", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_routes_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_routes_id"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_Guid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_route_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_route_addresses_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") @@ -246,6 +346,32 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Country"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => { b.Navigation("Addresses"); @@ -260,6 +386,11 @@ namespace Persistence.PostgreSql.Migrations { b.Navigation("Cities"); }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 009b9a6..43195c1 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -1,8 +1,8 @@ using System.Reflection; using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Options; +using cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters; namespace cuqmbr.TravelGuide.Persistence.PostgreSql; @@ -41,12 +41,3 @@ public class PostgreSqlDbContext : DbContext .HaveConversion(); } } - -public class VehicleTypeConverter : ValueConverter -{ - public VehicleTypeConverter() - : base( - v => v.Name, - v => VehicleType.FromName(v)) - { } -} diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index bd97758..a2d696f 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -17,6 +17,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork RegionRepository = new PostgreSqlRegionRepository(_dbContext); CityRepository = new PostgreSqlCityRepository(_dbContext); AddressRepository = new PostgreSqlAddressRepository(_dbContext); + RouteRepository = new PostgreSqlRouteRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -27,6 +28,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public AddressRepository AddressRepository { get; init; } + public RouteRepository RouteRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs new file mode 100644 index 0000000..1af92ff --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteRepository : + PostgreSqlBaseRepository, RouteRepository +{ + public PostgreSqlRouteRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs b/src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs new file mode 100644 index 0000000..50f5c9c --- /dev/null +++ b/src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters; + +public class VehicleTypeConverter : ValueConverter +{ + public VehicleTypeConverter() + : base( + v => v.Name, + v => VehicleType.FromName(v)) + { } +} From bd87ab9133c53f779ca290e092d0ebb37be19742 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 1 May 2025 20:54:59 +0300 Subject: [PATCH 07/35] fix VehicleType type convertation issue in InMemory datastore without the fix tests would fail --- src/Persistence/InMemory/InMemoryDbContext.cs | 27 +++++-------------- .../PostgreSql/PostgreSqlDbContext.cs | 2 +- .../TypeConverters/VehicleTypeConverter.cs | 4 +-- 3 files changed, 9 insertions(+), 24 deletions(-) rename src/Persistence/{PostgreSql => }/TypeConverters/VehicleTypeConverter.cs (71%) diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index fa0cbef..48d0c61 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,8 +1,7 @@ -// using System.Reflection; -// using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; -// using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using cuqmbr.TravelGuide.Persistence.TypeConverters; namespace cuqmbr.TravelGuide.Persistence.InMemory; @@ -19,28 +18,14 @@ public class InMemoryDbContext : DbContext protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - - // builder.HasPostgresEnum( - // "vehicle_type", - // VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); - // } protected override void ConfigureConventions( ModelConfigurationBuilder builder) { - // builder - // .Properties() - // .HaveColumnType("vehicle_type") - // .HaveConversion(); + builder + .Properties() + .HaveColumnType("vehicle_type") + .HaveConversion(); } } - -// public class VehicleTypeConverter : ValueConverter -// { -// public VehicleTypeConverter() -// : base( -// v => v.Name, -// v => VehicleType.FromName(v)) -// { } -// } diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 43195c1..814849e 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -2,7 +2,7 @@ using System.Reflection; using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -using cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters; +using cuqmbr.TravelGuide.Persistence.TypeConverters; namespace cuqmbr.TravelGuide.Persistence.PostgreSql; diff --git a/src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs b/src/Persistence/TypeConverters/VehicleTypeConverter.cs similarity index 71% rename from src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs rename to src/Persistence/TypeConverters/VehicleTypeConverter.cs index 50f5c9c..2274d4d 100644 --- a/src/Persistence/PostgreSql/TypeConverters/VehicleTypeConverter.cs +++ b/src/Persistence/TypeConverters/VehicleTypeConverter.cs @@ -1,11 +1,11 @@ using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters; +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; public class VehicleTypeConverter : ValueConverter { - public VehicleTypeConverter() + public VehicleTypeConverter() : base( v => v.Name, v => VehicleType.FromName(v)) From 09f3a46edc80c07df178c509bcdbc571715aed0c Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 1 May 2025 21:04:02 +0300 Subject: [PATCH 08/35] add more filter options for GetAddressesPageQuery --- .../GetAddressesPage/GetAddressesPageQuery.cs | 11 +++++++++++ .../GetAddressesPageQueryHandler.cs | 15 +++++++++++++++ .../ViewModels/GetAddressesPageFilterViewModel.cs | 10 ++++++++++ src/HttpApi/Controllers/AddressesController.cs | 9 +++++++++ 4 files changed, 45 insertions(+) diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs index deaae5a..36ed29a 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR; namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; @@ -18,4 +19,14 @@ public record GetAddressesPageQuery : IRequest> public Guid? RegionGuid { get; set; } public Guid? CityGuid { get; set; } + + public double? LongitudeGreaterOrEqualThan { get; set; } + + public double? LongitudeLessOrEqualThan { get; set; } + + public double? LatitudeGreaterOrEqualThan { get; set; } + + public double? LatitudeLessOrEqualThan { get; set; } + + public VehicleType? VehicleType { get; set; } } diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs index 0be0bca..9b4cd25 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs @@ -30,6 +30,21 @@ public class GetAddressesPageQueryHandler : e.City.Name.ToLower().Contains(request.Search.ToLower()) || e.City.Region.Name.ToLower().Contains(request.Search.ToLower()) || e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.LongitudeGreaterOrEqualThan != null + ? e.Longitude >= request.LongitudeGreaterOrEqualThan + : true) && + (request.LongitudeLessOrEqualThan != null + ? e.Longitude <= request.LongitudeLessOrEqualThan + : true) && + (request.LatitudeGreaterOrEqualThan != null + ? e.Latitude >= request.LatitudeGreaterOrEqualThan + : true) && + (request.LatitudeLessOrEqualThan != null + ? e.Latitude <= request.LatitudeLessOrEqualThan + : true) && + (request.VehicleType != null + ? e.VehicleType == request.VehicleType + : true) && (request.CityGuid != null ? e.City.Guid == request.CityGuid : true) && diff --git a/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs index 610f6c9..9dc7a20 100644 --- a/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs +++ b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs @@ -7,4 +7,14 @@ public sealed class GetAddressesPageFilterViewModel public Guid? RegionUuid { get; set; } public Guid? CityUuid { get; set; } + + public double? LongitudeGreaterOrEqualThan { get; set; } + + public double? LongitudeLessOrEqualThan { get; set; } + + public double? LatitudeGreaterOrEqualThan { get; set; } + + public double? LatitudeLessOrEqualThan { get; set; } + + public string? VehicleType { get; set; } } diff --git a/src/HttpApi/Controllers/AddressesController.cs b/src/HttpApi/Controllers/AddressesController.cs index 4d34676..825dca9 100644 --- a/src/HttpApi/Controllers/AddressesController.cs +++ b/src/HttpApi/Controllers/AddressesController.cs @@ -89,6 +89,15 @@ public class AddressesController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, + LongitudeGreaterOrEqualThan = + filterQuery.LongitudeGreaterOrEqualThan, + LongitudeLessOrEqualThan = + filterQuery.LongitudeLessOrEqualThan, + LatitudeGreaterOrEqualThan = + filterQuery.LatitudeGreaterOrEqualThan, + LatitudeLessOrEqualThan = + filterQuery.LatitudeLessOrEqualThan, + VehicleType = VehicleType.FromName(filterQuery.VehicleType), CountryGuid = filterQuery.CountryUuid, RegionGuid = filterQuery.RegionUuid, CityGuid = filterQuery.CityUuid From 201e6e7dfc8976ec344f4cd1583d3cf0ae2b62d9 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 2 May 2025 21:38:15 +0300 Subject: [PATCH 09/35] remove duplicate indices on pk and altk --- .../Configurations/BaseConfiguration.cs | 39 +- ...plicate_indices_on_pk_and_altk.Designer.cs | 352 ++++++++++++++++++ ...Remove_duplicate_indices_on_pk_and_altk.cs | 294 +++++++++++++++ .../PostgreSqlDbContextModelSnapshot.cs | 60 +-- 4 files changed, 668 insertions(+), 77 deletions(-) create mode 100644 src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs index 5905cc3..2407bbf 100644 --- a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -11,32 +11,23 @@ public class BaseConfiguration : IEntityTypeConfiguration { builder .HasKey(b => b.Id) - .HasName($"pk_{builder.Metadata.GetTableName()}"); + .HasName($"pk_{builder.Metadata.GetTableName() ?? + // Set primary key for inherited types using type name + // instead of mapped table name + builder.Metadata.ShortName().ToLower()}"); builder .Property(b => b.Id) .HasColumnName("id") .HasColumnType("bigint") .UseSequence( - $"{builder.Metadata.GetTableName()}_" + + $"{builder.Metadata.GetTableName() ?? + // Set sequence for inherited types using type name + // instead of mapped table name + builder.Metadata.ShortName().ToLower()}_" + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + "sequence"); - builder - .HasIndex(b => b.Id) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .HasAlternateKey(b => b.Guid) - .HasName( - "altk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); builder .Property(b => b.Guid) @@ -45,11 +36,13 @@ public class BaseConfiguration : IEntityTypeConfiguration .IsRequired(true); builder - .HasIndex(b => b.Guid) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") - .IsUnique(); + .HasAlternateKey(b => b.Guid) + .HasName( + "altk_" + + $"{builder.Metadata.GetTableName() ?? + // Set alternate key for inherited types using type name + // instead of mapped table name + builder.Metadata.ShortName().ToLower()}_" + + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs new file mode 100644 index 0000000..755f1db --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs @@ -0,0 +1,352 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250502183338_Remove_duplicate_indices_on_pk_and_altk")] + partial class Remove_duplicate_indices_on_pk_and_altk + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs new file mode 100644 index 0000000..92f844f --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs @@ -0,0 +1,294 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Remove_duplicate_indices_on_pk_and_altk : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "altk_routes_Guid", + schema: "application", + table: "routes"); + + migrationBuilder.DropIndex( + name: "ix_routes_id", + schema: "application", + table: "routes"); + + migrationBuilder.DropIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_route_addresses_Guid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_regions_Guid", + schema: "application", + table: "regions"); + + migrationBuilder.DropIndex( + name: "ix_regions_id", + schema: "application", + table: "regions"); + + migrationBuilder.DropIndex( + name: "ix_regions_uuid", + schema: "application", + table: "regions"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_countries_Guid", + schema: "application", + table: "countries"); + + migrationBuilder.DropIndex( + name: "ix_countries_id", + schema: "application", + table: "countries"); + + migrationBuilder.DropIndex( + name: "ix_countries_uuid", + schema: "application", + table: "countries"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_cities_Guid", + schema: "application", + table: "cities"); + + migrationBuilder.DropIndex( + name: "ix_cities_id", + schema: "application", + table: "cities"); + + migrationBuilder.DropIndex( + name: "ix_cities_uuid", + schema: "application", + table: "cities"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_addresses_Guid", + schema: "application", + table: "addresses"); + + migrationBuilder.DropIndex( + name: "ix_addresses_id", + schema: "application", + table: "addresses"); + + migrationBuilder.DropIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_routes_uuid", + schema: "application", + table: "routes", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_regions_uuid", + schema: "application", + table: "regions", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_countries_uuid", + schema: "application", + table: "countries", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_cities_uuid", + schema: "application", + table: "cities", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "altk_routes_uuid", + schema: "application", + table: "routes"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_route_addresses_uuid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_regions_uuid", + schema: "application", + table: "regions"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_countries_uuid", + schema: "application", + table: "countries"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_cities_uuid", + schema: "application", + table: "cities"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_addresses_uuid", + schema: "application", + table: "addresses"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_routes_Guid", + schema: "application", + table: "routes", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_route_addresses_Guid", + schema: "application", + table: "route_addresses", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_regions_Guid", + schema: "application", + table: "regions", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_countries_Guid", + schema: "application", + table: "countries", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_cities_Guid", + schema: "application", + table: "cities", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_addresses_Guid", + schema: "application", + table: "addresses", + column: "uuid"); + + migrationBuilder.CreateIndex( + name: "ix_routes_id", + schema: "application", + table: "routes", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_regions_id", + schema: "application", + table: "regions", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_regions_uuid", + schema: "application", + table: "regions", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_id", + schema: "application", + table: "countries", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_uuid", + schema: "application", + table: "countries", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_id", + schema: "application", + table: "cities", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_uuid", + schema: "application", + table: "cities", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_id", + schema: "application", + table: "addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid", + unique: true); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index f76eb3e..7b41978 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -73,19 +73,11 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_addresses"); b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); + .HasName("altk_addresses_uuid"); b.HasIndex("CityId") .HasDatabaseName("ix_addresses_city_id"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - b.ToTable("addresses", "application", t => { t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); @@ -119,15 +111,7 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_cities"); b.HasAlternateKey("Guid") - .HasName("altk_cities_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_cities_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_cities_id"); + .HasName("altk_cities_uuid"); b.HasIndex("RegionId") .HasDatabaseName("ix_cities_region_id"); @@ -158,15 +142,7 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_countries"); b.HasAlternateKey("Guid") - .HasName("altk_countries_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_countries_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_countries_id"); + .HasName("altk_countries_uuid"); b.ToTable("countries", "application"); }); @@ -198,19 +174,11 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_regions"); b.HasAlternateKey("Guid") - .HasName("altk_regions_Guid"); + .HasName("altk_regions_uuid"); b.HasIndex("CountryId") .HasDatabaseName("ix_regions_country_id"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_regions_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_regions_id"); - b.ToTable("regions", "application"); }); @@ -242,15 +210,7 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_routes"); b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); + .HasName("altk_routes_uuid"); b.ToTable("routes", "application", t => { @@ -288,7 +248,7 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_route_addresses"); b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + .HasName("altk_route_addresses_uuid"); b.HasAlternateKey("AddressId", "RouteId", "Order") .HasName("altk_route_addresses_address_id_route_id_order"); @@ -296,14 +256,6 @@ namespace Persistence.PostgreSql.Migrations b.HasIndex("AddressId") .HasDatabaseName("ix_route_addresses_address_id"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - b.HasIndex("RouteId") .HasDatabaseName("ix_route_addresses_route_id"); From 3ebd0c3a2c50c3a354427e331c2ed0e0be115c36 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sat, 3 May 2025 10:09:52 +0300 Subject: [PATCH 10/35] add vehicles hierarchy management --- src/Application/Aircrafts/AircraftDto.cs | 23 + .../AddAircraft/AddAircraftCommand.cs | 12 + .../AddAircraftCommandAuthorizer.cs | 31 ++ .../AddAircraft/AddAircraftCommandHandler.cs | 51 ++ .../AddAircraftCommandValidator.cs | 37 ++ .../DeleteAircraft/DeleteAircraftCommand.cs | 8 + .../DeleteAircraftCommandAuthorizer.cs | 31 ++ .../DeleteAircraftCommandHandler.cs | 34 ++ .../DeleteAircraftCommandValidator.cs | 14 + .../UpdateAircraft/UpdateAircraftCommand.cs | 14 + .../UpdateAircraftCommandAuthorizer.cs | 31 ++ .../UpdateAircraftCommandHandler.cs | 56 +++ .../UpdateAircraftCommandValidator.cs | 41 ++ .../Queries/GetAircraft/GetAircraftQuery.cs | 8 + .../GetAircraft/GetAircraftQueryAuthorizer.cs | 31 ++ .../GetAircraft/GetAircraftQueryHandler.cs | 38 ++ .../GetAircraft/GetAircraftQueryValidator.cs | 14 + .../GetAircraftsPage/GetAircraftsPageQuery.cs | 23 + .../GetAircraftsPageQueryAuthorizer.cs | 31 ++ .../GetAircraftsPageQueryHandler.cs | 53 ++ .../GetAircraftsPageQueryValidator.cs | 43 ++ .../ViewModels/AddAircraftViewModel.cs | 10 + .../GetAircraftsPageFilterViewModel.cs | 15 + .../ViewModels/UpdateAircraftViewModel.cs | 10 + src/Application/Buses/BusDto.cs | 23 + .../Buses/Commands/AddBus/AddBusCommand.cs | 12 + .../AddBus/AddBusCommandAuthorizer.cs | 31 ++ .../Commands/AddBus/AddBusCommandHandler.cs | 51 ++ .../Commands/AddBus/AddBusCommandValidator.cs | 37 ++ .../Commands/DeleteBus/DeleteBusCommand.cs | 8 + .../DeleteBus/DeleteBusCommandAuthorizer.cs | 31 ++ .../DeleteBus/DeleteBusCommandHandler.cs | 34 ++ .../DeleteBus/DeleteBusCommandValidator.cs | 14 + .../Commands/UpdateBus/UpdateBusCommand.cs | 14 + .../UpdateBus/UpdateBusCommandAuthorizer.cs | 31 ++ .../UpdateBus/UpdateBusCommandHandler.cs | 56 +++ .../UpdateBus/UpdateBusCommandValidator.cs | 41 ++ .../Buses/Queries/GetBus/GetBusQuery.cs | 8 + .../Queries/GetBus/GetBusQueryAuthorizer.cs | 31 ++ .../Queries/GetBus/GetBusQueryHandler.cs | 38 ++ .../Queries/GetBus/GetBusQueryValidator.cs | 14 + .../Queries/GetBusesPage/GetBusesPageQuery.cs | 23 + .../GetBusesPageQueryAuthorizer.cs | 31 ++ .../GetBusesPage/GetBusesPageQueryHandler.cs | 53 ++ .../GetBusesPageQueryValidator.cs | 43 ++ .../Buses/ViewModels/AddBusViewModel.cs | 10 + .../ViewModels/GetBusesPageFilterViewModel.cs | 15 + .../Buses/ViewModels/UpdateBusViewModel.cs | 10 + .../Repositories/AircraftRepository.cs | 6 + .../Persistence/Repositories/BusRepository.cs | 6 + .../Repositories/TrainRepository.cs | 6 + .../Repositories/VehicleRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 8 + .../Commands/AddTrain/AddTrainCommand.cs | 12 + .../AddTrain/AddTrainCommandAuthorizer.cs | 31 ++ .../AddTrain/AddTrainCommandHandler.cs | 51 ++ .../AddTrain/AddTrainCommandValidator.cs | 37 ++ .../DeleteTrain/DeleteTrainCommand.cs | 8 + .../DeleteTrainCommandAuthorizer.cs | 31 ++ .../DeleteTrain/DeleteTrainCommandHandler.cs | 34 ++ .../DeleteTrainCommandValidator.cs | 14 + .../UpdateTrain/UpdateTrainCommand.cs | 14 + .../UpdateTrainCommandAuthorizer.cs | 31 ++ .../UpdateTrain/UpdateTrainCommandHandler.cs | 56 +++ .../UpdateTrainCommandValidator.cs | 41 ++ .../Trains/Queries/GetTrain/GetTrainQuery.cs | 8 + .../GetTrain/GetTrainQueryAuthorizer.cs | 31 ++ .../Queries/GetTrain/GetTrainQueryHandler.cs | 38 ++ .../GetTrain/GetTrainQueryValidator.cs | 14 + .../GetTrainsPage/GetTrainsPageQuery.cs | 23 + .../GetTrainsPageQueryAuthorizer.cs | 31 ++ .../GetTrainsPageQueryHandler.cs | 53 ++ .../GetTrainsPageQueryValidator.cs | 43 ++ src/Application/Trains/TrainDto.cs | 23 + .../Trains/ViewModels/AddTrainViewModel.cs | 10 + .../GetTrainsPageFilterViewModel.cs | 15 + .../Trains/ViewModels/UpdateTrainViewModel.cs | 10 + .../Persistence/Configuration.cs | 1 + src/Domain/Entities/Aircraft.cs | 12 + src/Domain/Entities/Bus.cs | 12 + src/Domain/Entities/Train.cs | 12 + src/Domain/Entities/Vehicle.cs | 8 + .../Controllers/AircraftsController.cs | 193 +++++++ src/HttpApi/Controllers/BusesController.cs | 193 +++++++ src/HttpApi/Controllers/TestsController.cs | 27 +- src/HttpApi/Controllers/TrainsController.cs | 193 +++++++ src/HttpApi/appsettings.json | 31 ++ .../InMemory/InMemoryUnitOfWork.cs | 12 + .../InMemoryAircraftRepository.cs | 11 + .../Repositories/InMemoryBusRepository.cs | 11 + .../Repositories/InMemoryTrainRepository.cs | 11 + .../Repositories/InMemoryVehicleRepository.cs | 11 + .../Configurations/AircraftConfiguration.cs | 33 ++ .../Configurations/BaseConfiguration.cs | 21 +- .../Configurations/BusConfiguration.cs | 33 ++ .../Configurations/TrainConfiguration.cs | 33 ++ .../Configurations/VehicleConfiguration.cs | 41 ++ ...ft_Train_with_basic_properties.Designer.cs | 476 ++++++++++++++++++ ...us_Aircraft_Train_with_basic_properties.cs | 50 ++ .../PostgreSqlDbContextModelSnapshot.cs | 124 +++++ .../PostgreSql/PostgreSqlDbContext.cs | 1 - .../PostgreSql/PostgreSqlUnitOfWork.cs | 12 + .../PostgreSqlAircraftRepository.cs | 11 + .../Repositories/PostgreSqlBusRepository.cs | 11 + .../Repositories/PostgreSqlTrainRepository.cs | 11 + .../PostgreSqlVehicleRepository.cs | 11 + 106 files changed, 3599 insertions(+), 16 deletions(-) create mode 100644 src/Application/Aircrafts/AircraftDto.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs create mode 100644 src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs create mode 100644 src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs create mode 100644 src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs create mode 100644 src/Application/Buses/BusDto.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommand.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQuery.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs create mode 100644 src/Application/Buses/ViewModels/AddBusViewModel.cs create mode 100644 src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs create mode 100644 src/Application/Buses/ViewModels/UpdateBusViewModel.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs create mode 100644 src/Application/Trains/TrainDto.cs create mode 100644 src/Application/Trains/ViewModels/AddTrainViewModel.cs create mode 100644 src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs create mode 100644 src/Application/Trains/ViewModels/UpdateTrainViewModel.cs create mode 100644 src/Domain/Entities/Aircraft.cs create mode 100644 src/Domain/Entities/Bus.cs create mode 100644 src/Domain/Entities/Train.cs create mode 100644 src/Domain/Entities/Vehicle.cs create mode 100644 src/HttpApi/Controllers/AircraftsController.cs create mode 100644 src/HttpApi/Controllers/BusesController.cs create mode 100644 src/HttpApi/Controllers/TrainsController.cs create mode 100644 src/HttpApi/appsettings.json create mode 100644 src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/BusConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs new file mode 100644 index 0000000..8549160 --- /dev/null +++ b/src/Application/Aircrafts/AircraftDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Aircrafts; + +public sealed class AircraftDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs new file mode 100644 index 0000000..f938a36 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public record AddAircraftCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..8c98851 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs new file mode 100644 index 0000000..8ffe4e7 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Aircraft with given number already exists."); + } + + entity = new Aircraft() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.AircraftRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs new file mode 100644 index 0000000..76f5fe1 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandValidator : AbstractValidator +{ + public AddAircraftCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs new file mode 100644 index 0000000..88bd0c7 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public record DeleteAircraftCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..97f1463 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs new file mode 100644 index 0000000..2188c03 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAircraftCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AircraftRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs new file mode 100644 index 0000000..8d43bfc --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandValidator : AbstractValidator +{ + public DeleteAircraftCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs new file mode 100644 index 0000000..77a174e --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public record UpdateAircraftCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..c7036f5 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs new file mode 100644 index 0000000..952af00 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Aircraft with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.AircraftRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs new file mode 100644 index 0000000..70fb15a --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandValidator : AbstractValidator +{ + public UpdateAircraftCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs new file mode 100644 index 0000000..60fb5f1 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public record GetAircraftQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs new file mode 100644 index 0000000..121cf58 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAircraftQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAircraftQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs new file mode 100644 index 0000000..a92c658 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAircraftQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs new file mode 100644 index 0000000..c5a5e09 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryValidator : AbstractValidator +{ + public GetAircraftQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs new file mode 100644 index 0000000..df99a61 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public record GetAircraftsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs new file mode 100644 index 0000000..79a0546 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAircraftsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAircraftsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs new file mode 100644 index 0000000..e75b5f0 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAircraftsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AircraftRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs new file mode 100644 index 0000000..bed93b0 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryValidator : AbstractValidator +{ + public GetAircraftsPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs new file mode 100644 index 0000000..3a6e5bd --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class AddAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs new file mode 100644 index 0000000..c956d2b --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class GetAircraftsPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs new file mode 100644 index 0000000..1c4e71e --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class UpdateAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs new file mode 100644 index 0000000..3ba7ae5 --- /dev/null +++ b/src/Application/Buses/BusDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Buses; + +public sealed class BusDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs new file mode 100644 index 0000000..786a773 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public record AddBusCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs new file mode 100644 index 0000000..60a0581 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs new file mode 100644 index 0000000..c5a8488 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Bus with given number already exists."); + } + + entity = new Bus() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.BusRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs new file mode 100644 index 0000000..46bf658 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandValidator : AbstractValidator +{ + public AddBusCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs new file mode 100644 index 0000000..32ea1d6 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public record DeleteBusCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs new file mode 100644 index 0000000..2f61edc --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs new file mode 100644 index 0000000..f226338 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteBusCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.BusRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs new file mode 100644 index 0000000..c4e71a5 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandValidator : AbstractValidator +{ + public DeleteBusCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs new file mode 100644 index 0000000..9754c7a --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public record UpdateBusCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs new file mode 100644 index 0000000..17201fa --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs new file mode 100644 index 0000000..152e220 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Bus with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.BusRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs new file mode 100644 index 0000000..7097ab2 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandValidator : AbstractValidator +{ + public UpdateBusCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQuery.cs b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs new file mode 100644 index 0000000..8419f16 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public record GetBusQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs new file mode 100644 index 0000000..372b46e --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetBusQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetBusQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs new file mode 100644 index 0000000..26ba345 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetBusQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs new file mode 100644 index 0000000..b97a62f --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryValidator : AbstractValidator +{ + public GetBusQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs new file mode 100644 index 0000000..cf97c50 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public record GetBusesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs new file mode 100644 index 0000000..f51a43c --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetBusesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetBusesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs new file mode 100644 index 0000000..920ea9e --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetBusesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.BusRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs new file mode 100644 index 0000000..f72c75e --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryValidator : AbstractValidator +{ + public GetBusesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs new file mode 100644 index 0000000..4be8485 --- /dev/null +++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class AddBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs new file mode 100644 index 0000000..a05dd27 --- /dev/null +++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class GetBusesPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs new file mode 100644 index 0000000..6ee2c90 --- /dev/null +++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class UpdateBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs new file mode 100644 index 0000000..8ccd4ca --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface AircraftRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs new file mode 100644 index 0000000..18c76dc --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface BusRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs new file mode 100644 index 0000000..640a507 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TrainRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs new file mode 100644 index 0000000..db9fde8 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface VehicleRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index fde3ef8..e4cad5f 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -14,6 +14,14 @@ public interface UnitOfWork : IDisposable RouteRepository RouteRepository { get; } + VehicleRepository VehicleRepository { get; } + + BusRepository BusRepository { get; } + + AircraftRepository AircraftRepository { get; } + + TrainRepository TrainRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs new file mode 100644 index 0000000..a5e81d1 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public record AddTrainCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs new file mode 100644 index 0000000..36fdbf4 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs new file mode 100644 index 0000000..8edd60c --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Train with given number already exists."); + } + + entity = new Train() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.TrainRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs new file mode 100644 index 0000000..f8797c5 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandValidator : AbstractValidator +{ + public AddTrainCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs new file mode 100644 index 0000000..6d714c7 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public record DeleteTrainCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs new file mode 100644 index 0000000..df31b90 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs new file mode 100644 index 0000000..94d3ab0 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteTrainCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.TrainRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs new file mode 100644 index 0000000..f1008cd --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandValidator : AbstractValidator +{ + public DeleteTrainCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs new file mode 100644 index 0000000..4aa128f --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public record UpdateTrainCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs new file mode 100644 index 0000000..c4dd607 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs new file mode 100644 index 0000000..70ea0ce --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Train with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.TrainRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs new file mode 100644 index 0000000..689d2e8 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandValidator : AbstractValidator +{ + public UpdateTrainCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs new file mode 100644 index 0000000..9285936 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public record GetTrainQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs new file mode 100644 index 0000000..7416f04 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTrainQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTrainQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs new file mode 100644 index 0000000..b0c89fd --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetTrainQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs new file mode 100644 index 0000000..f3bcd7e --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryValidator : AbstractValidator +{ + public GetTrainQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs new file mode 100644 index 0000000..ba41889 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public record GetTrainsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs new file mode 100644 index 0000000..2a68aaa --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTrainsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTrainsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs new file mode 100644 index 0000000..2690cfa --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetTrainsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.TrainRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs new file mode 100644 index 0000000..1b56968 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryValidator : AbstractValidator +{ + public GetTrainsPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Trains/TrainDto.cs b/src/Application/Trains/TrainDto.cs new file mode 100644 index 0000000..2e02c73 --- /dev/null +++ b/src/Application/Trains/TrainDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Trains; + +public sealed class TrainDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Trains/ViewModels/AddTrainViewModel.cs b/src/Application/Trains/ViewModels/AddTrainViewModel.cs new file mode 100644 index 0000000..998097e --- /dev/null +++ b/src/Application/Trains/ViewModels/AddTrainViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class AddTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs new file mode 100644 index 0000000..ef0026b --- /dev/null +++ b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class GetTrainsPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs new file mode 100644 index 0000000..3b7682e --- /dev/null +++ b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class UpdateTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 1eb83be..396aeba 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -29,6 +29,7 @@ public static class Configuration configuration.ConnectionString, options => { + // TODO: Move to persistence project options.MigrationsHistoryTable( "ef_migrations_history", configuration.PartitionName); diff --git a/src/Domain/Entities/Aircraft.cs b/src/Domain/Entities/Aircraft.cs new file mode 100644 index 0000000..4401999 --- /dev/null +++ b/src/Domain/Entities/Aircraft.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Aircraft : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe aircraft's capabilities +} diff --git a/src/Domain/Entities/Bus.cs b/src/Domain/Entities/Bus.cs new file mode 100644 index 0000000..eed23c9 --- /dev/null +++ b/src/Domain/Entities/Bus.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Bus : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe bus' capabilities +} diff --git a/src/Domain/Entities/Train.cs b/src/Domain/Entities/Train.cs new file mode 100644 index 0000000..215c21b --- /dev/null +++ b/src/Domain/Entities/Train.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Train : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe train's capabilities +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs new file mode 100644 index 0000000..b0882df --- /dev/null +++ b/src/Domain/Entities/Vehicle.cs @@ -0,0 +1,8 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public abstract class Vehicle : EntityBase +{ + public VehicleType VehicleType { get; set; } +} diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs new file mode 100644 index 0000000..1aaa592 --- /dev/null +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Aircrafts; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("aircrafts")] +public class AircraftsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a aircraft")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAircraftCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all aircrafts")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAircraftsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAircraftsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a aircraft by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAircraftQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a aircraft")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAircraftCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a aircraft")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAircraftCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs new file mode 100644 index 0000000..48eb35d --- /dev/null +++ b/src/HttpApi/Controllers/BusesController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Buses; +using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; +using cuqmbr.TravelGuide.Application.Buses.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("buses")] +public class BusesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a bus")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddBusViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddBusCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all buses")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetBusesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetBusesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a bus by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetBusQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a bus")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateBusViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateBusCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a bus")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteBusCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index 4533d4e..b75627a 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -8,19 +9,41 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TestsController : ControllerBase { private readonly IStringLocalizer _localizer; + private readonly UnitOfWork _unitOfWork; public TestsController( CultureService cultureService, - IStringLocalizer localizer) + IStringLocalizer localizer, + UnitOfWork unitOfWork) { _localizer = localizer; + _unitOfWork = unitOfWork; } [HttpGet("getLocalizedString/{inputString}")] - public Task getLocalizedString( + public Task GetLocalizedString( [FromRoute] string inputString, CancellationToken cancellationToken) { return Task.FromResult(_localizer[inputString]); } + + [HttpGet("trigger")] + public async Task Trigger(CancellationToken cancellationToken) + { + // await _unitOfWork.BusRepository.AddOneAsync( + // new Domain.Entities.Bus() + // { + // Number = "AB1234MK", + // Model = "This is a fancy bus model", + // Capacity = 40 + // }, + // cancellationToken); + // + // await _unitOfWork.SaveAsync(cancellationToken); + // _unitOfWork.Dispose(); + + var vehicles = await _unitOfWork.VehicleRepository + .GetPageAsync(1, 10, cancellationToken); + } } diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs new file mode 100644 index 0000000..4d9baef --- /dev/null +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Trains; +using cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; +using cuqmbr.TravelGuide.Application.Trains.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("trains")] +public class TrainsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a train")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddTrainCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all trains")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetTrainsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetTrainsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a train by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetTrainQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a train")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateTrainCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a train")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteTrainCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json new file mode 100644 index 0000000..2bdff62 --- /dev/null +++ b/src/HttpApi/appsettings.json @@ -0,0 +1,31 @@ +{ + "Application": { + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + } + }, + "Identity": { + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + } + } +} diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index b719aad..e7faa4b 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -18,6 +18,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CityRepository = new InMemoryCityRepository(_dbContext); AddressRepository = new InMemoryAddressRepository(_dbContext); RouteRepository = new InMemoryRouteRepository(_dbContext); + VehicleRepository = new InMemoryVehicleRepository(_dbContext); + BusRepository = new InMemoryBusRepository(_dbContext); + AircraftRepository = new InMemoryAircraftRepository(_dbContext); + TrainRepository = new InMemoryTrainRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -30,6 +34,14 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public RouteRepository RouteRepository { get; init; } + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs new file mode 100644 index 0000000..86373d5 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAircraftRepository : + InMemoryBaseRepository, AircraftRepository +{ + public InMemoryAircraftRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs new file mode 100644 index 0000000..2180258 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryBusRepository : + InMemoryBaseRepository, BusRepository +{ + public InMemoryBusRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs new file mode 100644 index 0000000..603b223 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTrainRepository : + InMemoryBaseRepository, TrainRepository +{ + public InMemoryTrainRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs new file mode 100644 index 0000000..a0bd558 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleRepository : + InMemoryBaseRepository, VehicleRepository +{ + public InMemoryVehicleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs new file mode 100644 index 0000000..186e510 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AircraftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs index 2407bbf..c7a8bac 100644 --- a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -9,23 +9,21 @@ public class BaseConfiguration : IEntityTypeConfiguration { public virtual void Configure(EntityTypeBuilder builder) { + // Set table name for inherited types using type name + // instead of mapped table name + var tableName = builder.Metadata.GetTableName(); + builder .HasKey(b => b.Id) - .HasName($"pk_{builder.Metadata.GetTableName() ?? - // Set primary key for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}"); + .HasName($"pk_{tableName}"); builder .Property(b => b.Id) .HasColumnName("id") .HasColumnType("bigint") .UseSequence( - $"{builder.Metadata.GetTableName() ?? - // Set sequence for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}_" + - $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + + $"{tableName}_" + + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + "sequence"); @@ -39,10 +37,7 @@ public class BaseConfiguration : IEntityTypeConfiguration .HasAlternateKey(b => b.Guid) .HasName( "altk_" + - $"{builder.Metadata.GetTableName() ?? - // Set alternate key for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}_" + + $"{tableName}_" + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs new file mode 100644 index 0000000..593f72f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class BusConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs new file mode 100644 index 0000000..fccefeb --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class TrainConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs new file mode 100644 index 0000000..e95accf --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(a => a.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "vehicles", + v => v.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); + + builder + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); + + base.Configure(builder); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs new file mode 100644 index 0000000..d32eb83 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs @@ -0,0 +1,476 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250503053607_Add_Bus_Aircraft_Train_with_basic_properties")] + partial class Add_Bus_Aircraft_Train_with_basic_properties + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs new file mode 100644 index 0000000..b071d55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Bus_Aircraft_Train_with_basic_properties : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "vehicles_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicles_id_sequence')"), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + number = table.Column(type: "varchar(32)", nullable: true), + model = table.Column(type: "varchar(64)", nullable: true), + capacity = table.Column(type: "smallint", nullable: true), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicles", x => x.id); + table.UniqueConstraint("altk_vehicles_uuid", x => x.uuid); + table.CheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "vehicles", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicles_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 7b41978..8140f00 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -35,6 +35,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.HasSequence("vehicles_id_sequence"); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") @@ -262,6 +264,128 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("route_addresses", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 814849e..afede19 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -29,7 +29,6 @@ public class PostgreSqlDbContext : DbContext Assembly.GetExecutingAssembly(), t => t.Namespace == "cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations"); - } protected override void ConfigureConventions( diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index a2d696f..0642a3e 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -18,6 +18,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CityRepository = new PostgreSqlCityRepository(_dbContext); AddressRepository = new PostgreSqlAddressRepository(_dbContext); RouteRepository = new PostgreSqlRouteRepository(_dbContext); + VehicleRepository = new PostgreSqlVehicleRepository(_dbContext); + BusRepository = new PostgreSqlBusRepository(_dbContext); + AircraftRepository = new PostgreSqlAircraftRepository(_dbContext); + TrainRepository = new PostgreSqlTrainRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -30,6 +34,14 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public RouteRepository RouteRepository { get; init; } + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs new file mode 100644 index 0000000..292fcea --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAircraftRepository : + PostgreSqlBaseRepository, AircraftRepository +{ + public PostgreSqlAircraftRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs new file mode 100644 index 0000000..da4c844 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlBusRepository : + PostgreSqlBaseRepository, BusRepository +{ + public PostgreSqlBusRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs new file mode 100644 index 0000000..2dffccc --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTrainRepository : + PostgreSqlBaseRepository, TrainRepository +{ + public PostgreSqlTrainRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs new file mode 100644 index 0000000..da7cf91 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleRepository : + PostgreSqlBaseRepository, VehicleRepository +{ + public PostgreSqlVehicleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} From 5ee8c9c5df1150f495d3f41b874cc567a600b5c7 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sun, 11 May 2025 10:51:19 +0300 Subject: [PATCH 11/35] add vehicle enrollments management --- .../Repositories/RouteAddressRepository.cs | 7 + .../VehicleEnrollmentRepository.cs | 7 + .../Interfaces/Persistence/UnitOfWork.cs | 4 + .../Resources/Localization/en-US.json | 10 +- src/Application/Routes/RouteAddressDto.cs | 25 +- .../AddVehicleEnrollmentCommand.cs | 20 + .../AddVehicleEnrollmentCommandAuthorizer.cs | 32 + .../AddVehicleEnrollmentCommandHandler.cs | 214 ++++++ .../AddVehicleEnrollmentCommandValidator.cs | 63 ++ .../DeleteVehicleEnrollmentCommand.cs | 8 + ...eleteVehicleEnrollmentCommandAuthorizer.cs | 31 + .../DeleteVehicleEnrollmentCommandHandler.cs | 38 ++ ...DeleteVehicleEnrollmentCommandValidator.cs | 14 + .../UpdateVehicleEnrollmentCommand.cs | 18 + ...pdateVehicleEnrollmentCommandAuthorizer.cs | 33 + .../UpdateVehicleEnrollmentCommandHandler.cs | 191 ++++++ ...UpdateVehicleEnrollmentCommandValidator.cs | 58 ++ .../Models/RouteAddressDetailModel.cs | 13 + .../GetVehicleEnrollmentQuery.cs | 9 + .../GetVehicleEnrollmentQueryAuthorizer.cs | 32 + .../GetVehicleEnrollmentQueryHandler.cs | 58 ++ .../GetVehicleEnrollmentQueryValidator.cs | 14 + .../GetVehicleEnrollmentsPageQuery.cs | 52 ++ ...etVehicleEnrollmentsPageQueryAuthorizer.cs | 31 + .../GetVehicleEnrollmentsPageQueryHandler.cs | 157 +++++ ...GetVehicleEnrollmentsPageQueryValidator.cs | 43 ++ .../RouteAddressDetailDto.cs | 29 + .../VehicleEnrollmentDto.cs | 50 ++ .../VehicleEnrollmentRouteAddressDto.cs | 75 +++ .../AddVehicleEnrollmentViewModel.cs | 15 + ...etVehicleEnrollmentsPageFilterViewModel.cs | 46 ++ .../ViewModels/RouteAddressDetailViewModel.cs | 13 + .../UpdateVehicleEnrollmentViewModel.cs | 11 + src/Domain/Entities/Route.cs | 2 + src/Domain/Entities/RouteAddress.cs | 3 + src/Domain/Entities/RouteAddressDetail.cs | 20 + src/Domain/Entities/Vehicle.cs | 3 + src/Domain/Entities/VehicleEnrollment.cs | 23 + src/Domain/Enums/Currency.cs | 30 + .../Controllers/AddressesController.cs | 3 +- .../Controllers/AircraftsController.cs | 3 +- src/HttpApi/Controllers/BusesController.cs | 3 +- src/HttpApi/Controllers/CitiesController.cs | 3 +- .../Controllers/CountriesController.cs | 3 +- src/HttpApi/Controllers/RegionsController.cs | 7 +- src/HttpApi/Controllers/RoutesController.cs | 3 +- src/HttpApi/Controllers/TrainsController.cs | 3 +- .../VehicleEnrollmentsController.cs | 260 +++++++ src/Persistence/InMemory/InMemoryDbContext.cs | 5 + .../InMemory/InMemoryUnitOfWork.cs | 8 + .../InMemoryRouteAddressRepository.cs | 11 + .../InMemoryVehicleEnrollmentRepository.cs | 11 + src/Persistence/Json/JsonDbContext.cs | 0 .../RouteAddressDetailConfiguration.cs | 85 +++ .../VehicleEnrollmentConfiguration.cs | 88 +++ ...lment_and_Route_Address_Detail.Designer.cs | 637 ++++++++++++++++++ ...cle_Enrollment_and_Route_Address_Detail.cs | 133 ++++ .../PostgreSqlDbContextModelSnapshot.cs | 161 +++++ .../PostgreSql/PostgreSqlDbContext.cs | 11 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 8 + .../PostgreSqlRouteAddressRepository.cs | 11 + .../PostgreSqlVehicleEnrollmentRepository.cs | 11 + .../TypeConverters/CurrencyConverter.cs | 13 + .../TypeConverters/DateTimeOffsetConverter.cs | 15 + 64 files changed, 2976 insertions(+), 22 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs create mode 100644 src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs create mode 100644 src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs create mode 100644 src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs create mode 100644 src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs create mode 100644 src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs create mode 100644 src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs create mode 100644 src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs create mode 100644 src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs create mode 100644 src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs create mode 100644 src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs create mode 100644 src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs create mode 100644 src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs create mode 100644 src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs create mode 100644 src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs create mode 100644 src/Application/VehicleEnrollments/RouteAddressDetailDto.cs create mode 100644 src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs create mode 100644 src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs create mode 100644 src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs create mode 100644 src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs create mode 100644 src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs create mode 100644 src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs create mode 100644 src/Domain/Entities/RouteAddressDetail.cs create mode 100644 src/Domain/Entities/VehicleEnrollment.cs create mode 100644 src/Domain/Enums/Currency.cs create mode 100644 src/HttpApi/Controllers/VehicleEnrollmentsController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs delete mode 100644 src/Persistence/Json/JsonDbContext.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs create mode 100644 src/Persistence/TypeConverters/CurrencyConverter.cs create mode 100644 src/Persistence/TypeConverters/DateTimeOffsetConverter.cs diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs new file mode 100644 index 0000000..4ff5733 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface RouteAddressRepository : + BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs new file mode 100644 index 0000000..1341b74 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface VehicleEnrollmentRepository : + BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index e4cad5f..5cd0770 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -22,6 +22,10 @@ public interface UnitOfWork : IDisposable TrainRepository TrainRepository { get; } + VehicleEnrollmentRepository VehicleEnrollmentRepository { get; } + + RouteAddressRepository RouteAddressRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 86a35db..ff87349 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -8,7 +8,15 @@ }, "Validation": { "DistinctOrder": "Must have distinct order values.", - "SameVehicleType": "Must have the same vehicle type." + "SameVehicleType": "Must have the same vehicle type.", + "DateTimeOffset": { + "GreaterThanOrEqualTo": "Must be greater or equal to {0:U}" + }, + "VehicleEnrollments": { + "OverlapWithOther": "Provided vehicle enrollment overlapping in schedule with other one.", + "NegativeTime": "Specified time must be positive time span.", + "NegativeCost": "Specified cost must be positive value." + } }, "ExceptionHandling": { "ValidationException": { diff --git a/src/Application/Routes/RouteAddressDto.cs b/src/Application/Routes/RouteAddressDto.cs index 13288f0..fbe2148 100644 --- a/src/Application/Routes/RouteAddressDto.cs +++ b/src/Application/Routes/RouteAddressDto.cs @@ -5,18 +5,20 @@ namespace cuqmbr.TravelGuide.Application.Routes; public sealed class RouteAddressDto : IMapFrom { + public Guid RouteAddressUuid { get; set; } + public short Order { get; set; } - public Guid Uuid { get; set; } + public Guid AddressUuid { get; set; } - public string Name { get; set; } + public string AddressName { get; set; } - public double Longitude { get; set; } + public double AddressLongitude { get; set; } - public double Latitude { get; set; } + public double AddressLatitude { get; set; } - public string VehicleType { get; set; } + public string AddressVehicleType { get; set; } public Guid CountryUuid { get; set; } @@ -34,19 +36,22 @@ public sealed class RouteAddressDto : IMapFrom { profile.CreateMap() .ForMember( - d => d.Uuid, + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.AddressUuid, opt => opt.MapFrom(s => s.Address.Guid)) .ForMember( - d => d.Name, + d => d.AddressName, opt => opt.MapFrom(s => s.Address.Name)) .ForMember( - d => d.Longitude, + d => d.AddressLongitude, opt => opt.MapFrom(s => s.Address.Longitude)) .ForMember( - d => d.Latitude, + d => d.AddressLatitude, opt => opt.MapFrom(s => s.Address.Latitude)) .ForMember( - d => d.VehicleType, + d => d.AddressVehicleType, opt => opt.MapFrom(s => s.Address.VehicleType.Name)) .ForMember( d => d.CityUuid, diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..8f5c49e --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs @@ -0,0 +1,20 @@ +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public record AddVehicleEnrollmentCommand : IRequest +{ + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public Guid VehicleGuid { get; set; } + + public Guid RouteGuid { get; set; } + + public ICollection RouteAddressDetails { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..dbaeae9 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddVehicleEnrollmentCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..3f4dac9 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,214 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public AddVehicleEnrollmentCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + AddVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + // Check if the vehicle exists. + + var vehicle = await _unitOfWork.VehicleRepository.GetOneAsync( + e => e.Guid == request.VehicleGuid, cancellationToken); + + if (vehicle == null) + { + throw new NotFoundException( + $"Vehicle with Guid: {request.VehicleGuid} not found."); + } + + + // Check if the route exists. + + var route = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.RouteGuid, e => e.RouteAddresses, + cancellationToken); + + if (route == null) + { + throw new NotFoundException( + $"Route with Guid: {request.RouteGuid} not found."); + } + + + // Check if specified vehicle and route compatible. + + if (vehicle.VehicleType != route.VehicleType) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.VehicleGuid), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + + // Check that request has the same Route Addresses + // as a route data from datastore. + + var sameRouteAddresses = route.RouteAddresses.All( + ra => request.RouteAddressDetails.Any( + rad => rad.RouteAddressGuid == ra.Guid)); + + if (!sameRouteAddresses) + { + throw new NotFoundException( + $"Not all route addresses are found in a datastore."); + } + + // Check vehicle enrollments that might overlap with new one. + + var requestDepartureTime = request.DepartureTime; + + var requestTravelTime = + request.RouteAddressDetails.Aggregate( + TimeSpan.Zero, (sum, rad) => sum + + rad.TimeToNextAddress + rad.CurrentAddressStopTime); + + var requestArrivalTime = requestDepartureTime + requestTravelTime; + + var enrollmentHistory = + await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( + e => + e.Vehicle.Guid == request.VehicleGuid && + e.DepartureTime >= DateTimeOffset.UtcNow.AddDays(-7), + e => e.RouteAddressDetails, + 1, 200, cancellationToken); + + // Three cases are included: + // + // ---RD---------SD----------RA---> + // time + // + // ---RD---------SA----------RA---> + // time + // + // ---SD-----RD-------RA-----SA---> + // time + // Where: + // RD - request enrollment departure time + // RA - request enrollment arrival time + // SD - datastore enrollment (S for store) departure time + // SA - datastore enrollment (S for store) arrival time + + var overlappingWithOtherEnrollments = enrollmentHistory.Items + .Where(ve => + { + var departureTime = ve.DepartureTime; + + var arrivalTime = + ve.DepartureTime + + ve.RouteAddressDetails + .Aggregate( + TimeSpan.Zero, + (sum, rad) => sum + + rad.TimeToNextAddress + + rad.CurrentAddressStopTime); + + return + (departureTime >= requestDepartureTime && + departureTime <= requestArrivalTime) || + (arrivalTime >= requestDepartureTime && + arrivalTime <= requestArrivalTime) || + (departureTime <= requestDepartureTime && + arrivalTime >= requestArrivalTime); + }) + .Any(); + + if (overlappingWithOtherEnrollments) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.DepartureTime), + ErrorMessage = _localizer["Validation." + + "VehicleEnrollments.OverlapWithOther"] + } + }); + } + + + // Create entity and add to datastore. + + var entity = new VehicleEnrollment() + { + DepartureTime = request.DepartureTime, + Currency = request.Currency, + VehicleId = vehicle.Id, + RouteId = route.Id, + RouteAddressDetails = route.RouteAddresses + .OrderBy(ra => ra.Order) + .Select(ra => new RouteAddressDetail() + { + TimeToNextAddress = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .TimeToNextAddress, + CostToNextAddress = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .CostToNextAddress, + CurrentAddressStopTime = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .CurrentAddressStopTime, + RouteAddressId = ra.Id + }) + .ToArray() + }; + + entity = await _unitOfWork.VehicleEnrollmentRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + + // Hydrate vehicle enrollment with address information + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + request.RouteAddressDetails + .Select(rad => rad.RouteAddressGuid) + .Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), + cancellationToken); + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == rad.RouteAddressId); + } + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..c971787 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,63 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandValidator : + AbstractValidator +{ + public AddVehicleEnrollmentCommandValidator( + IStringLocalizer localizer, + CultureService cultureService, + TimeZoneService timeZoneService) + { + RuleFor(v => v.DepartureTime) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .Must(dt => dt >= DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.DateTimeOffset.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + + RuleFor(v => v.Currency) + .Must(c => Currency.Enumerations.ContainsValue(c)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Currency.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.RouteAddressDetails.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.RouteAddressDetails) + .Must(v => v.All(rad => rad.RouteAddressGuid != Guid.Empty)) + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .Must(v => v.All(rad => rad.TimeToNextAddress >= TimeSpan.Zero)) + .WithMessage(localizer["VehicleEnrollments.NegativeTime"]) + .Must(v => v.All(rad => rad.CostToNextAddress >= 0)) + .WithMessage(localizer["VehicleEnrollments.NegativeCost"]) + .Must(v => v.All(rad => rad.CurrentAddressStopTime >= TimeSpan.Zero)) + .WithMessage(localizer["VehicleEnrollments.NegativeTime"]); + + + RuleFor(v => v.VehicleGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.RouteGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..a11cbd2 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public record DeleteVehicleEnrollmentCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..ecc544b --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteVehicleEnrollmentCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..0dbb95b --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteVehicleEnrollmentCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for tickets bought for this enrollment. + // Decide whether to cancel tickets or do not allow deletion. + + await _unitOfWork.VehicleEnrollmentRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..0126b0b --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandValidator : AbstractValidator +{ + public DeleteVehicleEnrollmentCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..918eda7 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs @@ -0,0 +1,18 @@ +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public record UpdateVehicleEnrollmentCommand : IRequest +{ + public Guid Guid { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public ICollection RouteAddressDetails { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..9831597 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..1b2d0b7 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,191 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public UpdateVehicleEnrollmentCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + // TODO: Check for tickets bought for this enrollment. + // Decide whether allow or not to perform update action. + + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddressDetails, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // Check that request has the same Route Addresses + // as a route data from datastore. + + var route = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Id == entity.RouteId, e => e.RouteAddresses, + cancellationToken); + + var sameRouteAddresses = route.RouteAddresses.All( + ra => request.RouteAddressDetails.Any( + rad => rad.RouteAddressGuid == ra.Guid)); + + if (!sameRouteAddresses) + { + throw new NotFoundException( + $"Not all route addresses are found in a datastore."); + } + + + // Check vehicle enrollments that might overlap with updated one. + // Exclude this vehicle enrollment. + + var requestDepartureTime = request.DepartureTime; + + var requestTravelTime = + request.RouteAddressDetails.Aggregate( + TimeSpan.Zero, (sum, rad) => sum + + rad.TimeToNextAddress + rad.CurrentAddressStopTime); + + var requestArrivalTime = requestDepartureTime + requestTravelTime; + + var enrollmentHistory = + await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( + e => + e.Vehicle.Id == entity.VehicleId && + e.Id != entity.Id && + e.DepartureTime >= DateTimeOffset.UtcNow.AddDays(-7), + e => e.RouteAddressDetails, + 1, 200, cancellationToken); + + // Three cases are included: + // + // ---RD---------SD----------RA---> + // time + // + // ---RD---------SA----------RA---> + // time + // + // ---SD-----RD-------RA-----SA---> + // time + // Where: + // RD - request enrollment departure time + // RA - request enrollment arrival time + // SD - datastore enrollment (S for store) departure time + // SA - datastore enrollment (S for store) arrival time + + var overlappingWithOtherEnrollments = enrollmentHistory.Items + .Where(ve => + { + var departureTime = ve.DepartureTime; + + var arrivalTime = + ve.DepartureTime + + ve.RouteAddressDetails + .Aggregate( + TimeSpan.Zero, + (sum, rad) => sum + + rad.TimeToNextAddress + + rad.CurrentAddressStopTime); + + return + (departureTime >= requestDepartureTime && + departureTime <= requestArrivalTime) || + (arrivalTime >= requestDepartureTime && + arrivalTime <= requestArrivalTime) || + (departureTime <= requestDepartureTime && + arrivalTime >= requestArrivalTime); + }) + .Any(); + + if (overlappingWithOtherEnrollments) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.DepartureTime), + ErrorMessage = _localizer["Validation." + + "VehicleEnrollments.OverlapWithOther"] + } + }); + } + + + // Update entity and add to datastore. + + entity.DepartureTime = request.DepartureTime; + entity.Currency = request.Currency; + + foreach (var rad in entity.RouteAddressDetails) + { + var correspondingRouteAddress = route.RouteAddresses + .First(ra => ra.Id == rad.RouteAddressId); + + rad.TimeToNextAddress = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .TimeToNextAddress; + rad.CostToNextAddress = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .CostToNextAddress; + rad.CurrentAddressStopTime = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .CurrentAddressStopTime; + } + + entity = await _unitOfWork.VehicleEnrollmentRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + + // Hydrate vehicle enrollment with address information + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + entity.RouteAddressDetails + .Select(rad => rad.RouteAddressId) + .Contains(e.Id), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), + cancellationToken); + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == rad.RouteAddressId); + } + + entity.RouteAddressDetails = entity.RouteAddressDetails + .OrderBy(rad => rad.RouteAddress.Order) + .ToArray(); + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..c15552f --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,58 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandValidator : + AbstractValidator +{ + public UpdateVehicleEnrollmentCommandValidator( + IStringLocalizer localizer, + CultureService cultureService, + TimeZoneService timeZoneService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureTime) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .Must(dt => dt >= DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.DateTimeOffset.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + + RuleFor(v => v.Currency) + .Must(c => Currency.Enumerations.ContainsValue(c)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Currency.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.RouteAddressDetails.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.RouteAddressDetails) + .Must(v => v.All(rad => rad.RouteAddressGuid != Guid.Empty)) + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .Must(v => v.All(rad => rad.TimeToNextAddress >= TimeSpan.Zero)) + .WithMessage(localizer["VehicleEnrollments.NegativeTime"]) + .Must(v => v.All(rad => rad.CostToNextAddress >= 0)) + .WithMessage(localizer["VehicleEnrollments.NegativeCost"]) + .Must(v => v.All(rad => rad.CurrentAddressStopTime >= TimeSpan.Zero)) + .WithMessage(localizer["VehicleEnrollments.NegativeTime"]); + } +} diff --git a/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs b/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs new file mode 100644 index 0000000..e3d972c --- /dev/null +++ b/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; + +public sealed class RouteAddressDetailModel +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public Guid RouteAddressGuid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs new file mode 100644 index 0000000..4220c6f --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public record GetVehicleEnrollmentQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs new file mode 100644 index 0000000..58fe68f --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetVehicleEnrollmentQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetVehicleEnrollmentQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs new file mode 100644 index 0000000..5ca9b2c --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs @@ -0,0 +1,58 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetVehicleEnrollmentQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetVehicleEnrollmentQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddressDetails, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // Hydrate vehicle enrollment with address information + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + entity.RouteAddressDetails + .Select(rad => rad.RouteAddressId) + .Contains(e.Id), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), + cancellationToken); + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == rad.RouteAddressId); + } + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs new file mode 100644 index 0000000..8a5931f --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryValidator : AbstractValidator +{ + public GetVehicleEnrollmentQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs new file mode 100644 index 0000000..a2c07bb --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs @@ -0,0 +1,52 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; + +public record GetVehicleEnrollmentsPageQuery : + IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + // public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? RouteGuid { get; set; } + + public Guid? VehicleGuid { get; set; } + + public int? NumberOfAddressesGreaterThanOrEqual { get; set; } + + public int? NumberOfAddressesLessThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqual { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqual { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqual { get; set; } + + public TimeSpan? TimeMovingGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeMovingLessThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsLessThanOrEqual { get; set; } + + public decimal? CostGreaterThanOrEqual { get; set; } + + public decimal? CostLessThanOrEqual { get; set; } + + public Currency? Currency { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs new file mode 100644 index 0000000..fc83c92 --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetVehicleEnrollmentsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetVehicleEnrollmentsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs new file mode 100644 index 0000000..ee4f86c --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs @@ -0,0 +1,157 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetVehicleEnrollmentsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetVehicleEnrollmentsPageQuery request, + CancellationToken cancellationToken) + { + // TODO: Add search functionality or remove it + var paginatedList = await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( + e => + // (e.Name.ToLower().Contains(request.Search.ToLower()) || + // e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RouteGuid != null + ? e.Route.Guid == request.RouteGuid + : true) && + (request.VehicleGuid != null + ? e.Vehicle.Guid >= request.VehicleGuid + : true) && + (request.NumberOfAddressesGreaterThanOrEqual != null + ? + e.RouteAddressDetails.Count() >= + request.NumberOfAddressesGreaterThanOrEqual + : true) && + (request.NumberOfAddressesLessThanOrEqual != null + ? + e.RouteAddressDetails.Count() <= + request.NumberOfAddressesLessThanOrEqual + : true) && + (request.DepartureTimeGreaterThanOrEqual != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqual + : true) && + (request.DepartureTimeLessThanOrEqual != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqual + : true) && + (request.ArrivalTimeGreaterThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) >= + request.ArrivalTimeGreaterThanOrEqual + : true) && + (request.ArrivalTimeLessThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) <= + request.ArrivalTimeLessThanOrEqual + : true) && + (request.TravelTimeGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TravelTimeGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TravelTimeLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TravelTimeLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TimeMovingGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TimeMovingLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) >= + request.TimeInStopsGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) <= + request.TimeInStopsLessThanOrEqual.Value.TotalSeconds + : true) && + (request.CostGreaterThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) >= + request.CostGreaterThanOrEqual + : true) && + (request.CostLessThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) <= + request.CostLessThanOrEqual + : true) && + (request.Currency != null + ? e.Currency == request.Currency + : true), + e => e.RouteAddressDetails, + request.PageNumber, request.PageSize, + cancellationToken); + + + // Hydrate vehicle enrollment with address information + + var routeAddressIds = paginatedList.Items + .SelectMany(ve => ve.RouteAddressDetails) + .Select(rad => rad.RouteAddressId); + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressIds.Contains(e.Id), + e => e.Address.City.Region.Country, + 1, paginatedList.Items.Sum(e => e.RouteAddressDetails.Count()), + cancellationToken); + + foreach (var vehicleEnrollment in paginatedList.Items) + { + foreach (var routeAddressDetail in + vehicleEnrollment.RouteAddressDetails) + { + routeAddressDetail.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == routeAddressDetail.RouteAddressId); + } + } + + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs new file mode 100644 index 0000000..9cfb2d3 --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryValidator : AbstractValidator +{ + public GetVehicleEnrollmentsPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + // RuleFor(v => v.Search) + // .MaximumLength(64) + // .WithMessage( + // String.Format( + // cultureService.Culture, + // localizer["FluentValidation.MaximumLength"], + // 64)); + } +} diff --git a/src/Application/VehicleEnrollments/RouteAddressDetailDto.cs b/src/Application/VehicleEnrollments/RouteAddressDetailDto.cs new file mode 100644 index 0000000..fe8d741 --- /dev/null +++ b/src/Application/VehicleEnrollments/RouteAddressDetailDto.cs @@ -0,0 +1,29 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class RouteAddressDetailDto : IMapFrom +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public Guid RouteAddressUuid { get; set; } + + + public VehicleEnrollmentRouteAddressDto RouteAddress { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.RouteAddress.Guid)) + .ForMember( + d => d.RouteAddress, + opt => opt.MapFrom(s => s.RouteAddress)); + } +} diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs new file mode 100644 index 0000000..0da091a --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs @@ -0,0 +1,50 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime => + DepartureTime + + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + public TimeSpan TravelTime => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + public TimeSpan TimeMoving => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress); + + public TimeSpan TimeInStops => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.CurrentAddressStopTime); + + public decimal TotalCost => + RouteAddressDetails.Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + public string Currency { get; set; } + + public ICollection RouteAddressDetails { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.DepartureTime, + opt => opt + .MapFrom( + s => s.DepartureTime)); + } +} diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs new file mode 100644 index 0000000..dae8127 --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs @@ -0,0 +1,75 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentRouteAddressDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public short Order { get; set; } + + + public Guid AddressUuid { get; set; } + + public string AddressName { get; set; } + + public double AddressLongitude { get; set; } + + public double AddressLatitude { get; set; } + + public string AddressVehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.AddressUuid, + opt => opt.MapFrom(s => s.Address.Guid)) + .ForMember( + d => d.AddressName, + opt => opt.MapFrom(s => s.Address.Name)) + .ForMember( + d => d.AddressLongitude, + opt => opt.MapFrom(s => s.Address.Longitude)) + .ForMember( + d => d.AddressLatitude, + opt => opt.MapFrom(s => s.Address.Latitude)) + .ForMember( + d => d.AddressVehicleType, + opt => opt.MapFrom(s => s.Address.VehicleType.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.Address.City.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Address.City.Region.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Name)); + } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs new file mode 100644 index 0000000..dd09bcf --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class AddVehicleEnrollmentViewModel +{ + public DateTimeOffset DepartureTime { get; set; } + + public string Currency { get; set; } + + + public Guid VehicleUuid { get; set; } + + public Guid RouteUuid { get; set; } + + public ICollection RouteAddressDetails { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs new file mode 100644 index 0000000..5cde753 --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs @@ -0,0 +1,46 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class GetVehicleEnrollmentsPageFilterViewModel +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? RouteGuid { get; set; } + + public Guid? VehicleGuid { get; set; } + + public int? NumberOfAddressesGreaterThanOrEqual { get; set; } + + public int? NumberOfAddressesLessThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqual { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqual { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqual { get; set; } + + public TimeSpan? TimeMovingGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeMovingLessThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsLessThanOrEqual { get; set; } + + public decimal? CostGreaterThanOrEqual { get; set; } + + public decimal? CostLessThanOrEqual { get; set; } + + public string? Currency { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs new file mode 100644 index 0000000..e1aaa6f --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class RouteAddressDetailViewModel +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public Guid RouteAddressUuid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs new file mode 100644 index 0000000..5c61d67 --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class UpdateVehicleEnrollmentViewModel +{ + public DateTimeOffset DepartureTime { get; set; } + + public string Currency { get; set; } + + + public ICollection RouteAddressDetails { get; set; } +} diff --git a/src/Domain/Entities/Route.cs b/src/Domain/Entities/Route.cs index f947c43..c4a0920 100644 --- a/src/Domain/Entities/Route.cs +++ b/src/Domain/Entities/Route.cs @@ -10,4 +10,6 @@ public sealed class Route : EntityBase public ICollection RouteAddresses { get; set; } + + public ICollection VehicleEnrollments { get; set; } } diff --git a/src/Domain/Entities/RouteAddress.cs b/src/Domain/Entities/RouteAddress.cs index f216cca..d383ff5 100644 --- a/src/Domain/Entities/RouteAddress.cs +++ b/src/Domain/Entities/RouteAddress.cs @@ -13,4 +13,7 @@ public sealed class RouteAddress : EntityBase public long RouteId { get; set; } public Route Route { get; set; } + + + public ICollection Details { get; set; } } diff --git a/src/Domain/Entities/RouteAddressDetail.cs b/src/Domain/Entities/RouteAddressDetail.cs new file mode 100644 index 0000000..3c4a6e4 --- /dev/null +++ b/src/Domain/Entities/RouteAddressDetail.cs @@ -0,0 +1,20 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class RouteAddressDetail : EntityBase +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } + + + public long RouteAddressId { get; set; } + + public RouteAddress RouteAddress { get; set; } +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs index b0882df..42196e8 100644 --- a/src/Domain/Entities/Vehicle.cs +++ b/src/Domain/Entities/Vehicle.cs @@ -5,4 +5,7 @@ namespace cuqmbr.TravelGuide.Domain.Entities; public abstract class Vehicle : EntityBase { public VehicleType VehicleType { get; set; } + + + public ICollection Enrollments { get; set; } } diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs new file mode 100644 index 0000000..ac92112 --- /dev/null +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class VehicleEnrollment : EntityBase +{ + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public long VehicleId { get; set; } + + public Vehicle Vehicle { get; set; } + + + public long RouteId { get; set; } + + public Route Route { get; set; } + + + public ICollection RouteAddressDetails { get; set; } +} diff --git a/src/Domain/Enums/Currency.cs b/src/Domain/Enums/Currency.cs new file mode 100644 index 0000000..4960808 --- /dev/null +++ b/src/Domain/Enums/Currency.cs @@ -0,0 +1,30 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +// ISO-4217 Country Codes dated 2025-03-31 + +public abstract class Currency : Enumeration +{ + public static readonly Currency USD = new USDCurrency(); + public static readonly Currency EUR = new EURCurrency(); + public static readonly Currency UAH = new UAHCurrency(); + + protected Currency(int value, string name) : base(value, name) { } + + private sealed class USDCurrency : Currency + { + public USDCurrency() : base(840, "USD") { } + } + + private sealed class EURCurrency : Currency + { + public EURCurrency() : base(978, "EUR") { } + } + + private sealed class UAHCurrency : Currency + { + public UAHCurrency() : base(980, "UAH") { } + } +} diff --git a/src/HttpApi/Controllers/AddressesController.cs b/src/HttpApi/Controllers/AddressesController.cs index 825dca9..2d8c8c1 100644 --- a/src/HttpApi/Controllers/AddressesController.cs +++ b/src/HttpApi/Controllers/AddressesController.cs @@ -189,7 +189,8 @@ public class AddressesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(AddressDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs index 1aaa592..2c040e8 100644 --- a/src/HttpApi/Controllers/AircraftsController.cs +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -177,7 +177,8 @@ public class AircraftsController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs index 48eb35d..c5d55a5 100644 --- a/src/HttpApi/Controllers/BusesController.cs +++ b/src/HttpApi/Controllers/BusesController.cs @@ -177,7 +177,8 @@ public class BusesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/CitiesController.cs b/src/HttpApi/Controllers/CitiesController.cs index 086a5ad..73bb49d 100644 --- a/src/HttpApi/Controllers/CitiesController.cs +++ b/src/HttpApi/Controllers/CitiesController.cs @@ -172,7 +172,8 @@ public class CitiesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/CountriesController.cs b/src/HttpApi/Controllers/CountriesController.cs index 76dae58..056245e 100644 --- a/src/HttpApi/Controllers/CountriesController.cs +++ b/src/HttpApi/Controllers/CountriesController.cs @@ -160,7 +160,8 @@ public class CountriesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(CountryDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs index 478ec03..92d1d4b 100644 --- a/src/HttpApi/Controllers/RegionsController.cs +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -9,7 +9,6 @@ using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; using cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; -using cuqmbr.TravelGuide.Application.Regions.ViewModels; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -49,7 +48,8 @@ public class RegionsController : ControllerBase await Mediator.Send( new AddRegionCommand() { - Name = viewModel.Name + Name = viewModel.Name, + CountryGuid = viewModel.CountryUuid }, cancellationToken)); } @@ -171,7 +171,8 @@ public class RegionsController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(RegionDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/RoutesController.cs b/src/HttpApi/Controllers/RoutesController.cs index 1735a76..7eb6850 100644 --- a/src/HttpApi/Controllers/RoutesController.cs +++ b/src/HttpApi/Controllers/RoutesController.cs @@ -182,7 +182,8 @@ public class RoutesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs index 4d9baef..fac873a 100644 --- a/src/HttpApi/Controllers/TrainsController.cs +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -177,7 +177,8 @@ public class TrainsController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/VehicleEnrollmentsController.cs b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs new file mode 100644 index 0000000..b5a703c --- /dev/null +++ b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs @@ -0,0 +1,260 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.VehicleEnrollments; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.DeleteVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("vehicleEnrollments")] +public class VehicleEnrollmentsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a vehicle enrollment")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, + "Enrollment travel time overlapping with " + + "other enrollment time of the vehicle", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Given route not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Given vehicle not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "At least one route address not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddVehicleEnrollmentViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddVehicleEnrollmentCommand() + { + DepartureTime = viewModel.DepartureTime, + Currency = Currency.FromName(viewModel.Currency), + VehicleGuid = viewModel.VehicleUuid, + RouteGuid = viewModel.RouteUuid, + RouteAddressDetails = viewModel.RouteAddressDetails.Select( + rad => new RouteAddressDetailModel() + { + TimeToNextAddress = rad.TimeToNextAddress, + CostToNextAddress = rad.CostToNextAddress, + CurrentAddressStopTime = rad.CurrentAddressStopTime, + RouteAddressGuid = rad.RouteAddressUuid + }) + .ToArray() + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all vehicle enrollments")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetVehicleEnrollmentsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetVehicleEnrollmentsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + // Search = searchQuery.Search, + Sort = sortQuery.Sort, + RouteGuid = filterQuery.RouteGuid, + VehicleGuid = filterQuery.VehicleGuid, + NumberOfAddressesGreaterThanOrEqual = + filterQuery.NumberOfAddressesGreaterThanOrEqual, + NumberOfAddressesLessThanOrEqual = + filterQuery.NumberOfAddressesLessThanOrEqual, + DepartureTimeGreaterThanOrEqual = + filterQuery.DepartureTimeGreaterThanOrEqual, + DepartureTimeLessThanOrEqual = + filterQuery.DepartureTimeLessThanOrEqual, + ArrivalTimeGreaterThanOrEqual = + filterQuery.ArrivalTimeGreaterThanOrEqual, + ArrivalTimeLessThanOrEqual = + filterQuery.ArrivalTimeLessThanOrEqual, + TravelTimeGreaterThanOrEqual = + filterQuery.TravelTimeGreaterThanOrEqual, + TravelTimeLessThanOrEqual = + filterQuery.TravelTimeLessThanOrEqual, + TimeMovingGreaterThanOrEqual = + filterQuery.TimeMovingGreaterThanOrEqual, + TimeMovingLessThanOrEqual = + filterQuery.TimeMovingLessThanOrEqual, + TimeInStopsGreaterThanOrEqual = + filterQuery.TimeInStopsGreaterThanOrEqual, + TimeInStopsLessThanOrEqual = + filterQuery.TimeInStopsLessThanOrEqual, + CostGreaterThanOrEqual = + filterQuery.CostGreaterThanOrEqual, + CostLessThanOrEqual = + filterQuery.CostLessThanOrEqual, + Currency = Currency.FromName(filterQuery.Currency) + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a vehicle enrollment by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetVehicleEnrollmentQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a vehicle enrollment")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, + "Enrollment travel time overlapping with " + + "other enrollment time of the vehicle", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "At least one route address not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateVehicleEnrollmentViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateVehicleEnrollmentCommand() + { + Guid = uuid, + DepartureTime = viewModel.DepartureTime, + Currency = Currency.FromName(viewModel.Currency), + RouteAddressDetails = viewModel.RouteAddressDetails.Select( + rad => new RouteAddressDetailModel() + { + TimeToNextAddress = rad.TimeToNextAddress, + CostToNextAddress = rad.CostToNextAddress, + CurrentAddressStopTime = rad.CurrentAddressStopTime, + RouteAddressGuid = rad.RouteAddressUuid + }) + .ToArray() + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a vehicle enrollment")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteVehicleEnrollmentCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 48d0c61..9b2f919 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -27,5 +27,10 @@ public class InMemoryDbContext : DbContext .Properties() .HaveColumnType("vehicle_type") .HaveConversion(); + + builder + .Properties() + .HaveColumnType("currency") + .HaveConversion(); } } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index e7faa4b..cde7f75 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -22,6 +22,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork BusRepository = new InMemoryBusRepository(_dbContext); AircraftRepository = new InMemoryAircraftRepository(_dbContext); TrainRepository = new InMemoryTrainRepository(_dbContext); + VehicleEnrollmentRepository = + new InMemoryVehicleEnrollmentRepository(_dbContext); + RouteAddressRepository = + new InMemoryRouteAddressRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -42,6 +46,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public TrainRepository TrainRepository { get; init; } + public VehicleEnrollmentRepository VehicleEnrollmentRepository { get; init; } + + public RouteAddressRepository RouteAddressRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs new file mode 100644 index 0000000..ce24624 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteAddressRepository : + InMemoryBaseRepository, RouteAddressRepository +{ + public InMemoryRouteAddressRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs new file mode 100644 index 0000000..2bf6313 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleEnrollmentRepository : + InMemoryBaseRepository, VehicleEnrollmentRepository +{ + public InMemoryVehicleEnrollmentRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/Json/JsonDbContext.cs b/src/Persistence/Json/JsonDbContext.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs new file mode 100644 index 0000000..56c95cb --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs @@ -0,0 +1,85 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteAddressDetailConfiguration : + BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("route_address_details"); + + base.Configure(builder); + + + builder + .Property(rad => rad.TimeToNextAddress) + .HasColumnName("time_to_next_address") + .HasColumnType("interval") + .IsRequired(true); + + builder + .Property(rad => rad.CostToNextAddress) + .HasColumnName("cost_to_next_address") + .HasColumnType("numeric(24,12)") + .IsRequired(true); + + builder + .Property(rad => rad.CurrentAddressStopTime) + .HasColumnName("current_address_stop_time") + .HasColumnType("interval") + .IsRequired(true); + + + builder + .Property(rad => rad.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + + builder + .HasOne(rad => rad.VehicleEnrollment) + .WithMany(ve => ve.RouteAddressDetails) + .HasForeignKey(rad => rad.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.VehicleEnrollmentId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rad => rad.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.VehicleEnrollmentId).Metadata.GetColumnName()}"); + + + builder + .Property(rad => rad.RouteAddressId) + .HasColumnName("route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(rad => rad.RouteAddress) + .WithMany(ra => ra.Details) + .HasForeignKey(rad => rad.RouteAddressId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.RouteAddressId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rad => rad.RouteAddressId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.RouteAddressId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs new file mode 100644 index 0000000..3c5ce9f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs @@ -0,0 +1,88 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleEnrollmentConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(ve => ve.Currency) + .HasColumnName("currency") + .HasColumnType("varchar(8)") + .IsRequired(true); + + builder + .ToTable( + "vehicle_enrollments", + ve => ve.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()}", + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Currency.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(ve => ve.DepartureTime) + .HasColumnName("departure_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + + builder + .Property(ve => ve.VehicleId) + .HasColumnName("vehicle_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ve => ve.Vehicle) + .WithMany(v => v.Enrollments) + .HasForeignKey(ve => ve.VehicleId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.VehicleId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ve => ve.VehicleId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.VehicleId).Metadata.GetColumnName()}"); + + + builder + .Property(ve => ve.RouteId) + .HasColumnName("route_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ve => ve.Route) + .WithMany(r => r.VehicleEnrollments) + .HasForeignKey(ve => ve.RouteId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.RouteId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ve => ve.RouteId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.RouteId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs new file mode 100644 index 0000000..8dcb5cd --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs @@ -0,0 +1,637 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail")] + partial class Add_Vehicle_Enrollment_and_Route_Address_Detail + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs new file mode 100644 index 0000000..2ee55fb --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Vehicle_Enrollment_and_Route_Address_Detail : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "route_address_details_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "vehicle_enrollments_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicle_enrollments", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicle_enrollments_id_sequence')"), + departure_time = table.Column(type: "timestamptz", nullable: false), + currency = table.Column(type: "varchar(8)", nullable: false), + vehicle_id = table.Column(type: "bigint", nullable: false), + route_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicle_enrollments", x => x.id); + table.UniqueConstraint("altk_vehicle_enrollments_uuid", x => x.uuid); + table.CheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + table.ForeignKey( + name: "fk_vehicle_enrollments_route_id", + column: x => x.route_id, + principalSchema: "application", + principalTable: "routes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_vehicle_enrollments_vehicle_id", + column: x => x.vehicle_id, + principalSchema: "application", + principalTable: "vehicles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "route_address_details", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_address_details_id_sequence')"), + time_to_next_address = table.Column(type: "interval", nullable: false), + cost_to_next_address = table.Column(type: "numeric(24,12)", nullable: false), + current_address_stop_time = table.Column(type: "interval", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + route_address_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_route_address_details", x => x.id); + table.UniqueConstraint("altk_route_address_details_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_route_address_details_route_address_id", + column: x => x.route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_route_address_details_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_route_address_details_route_address_id", + schema: "application", + table: "route_address_details", + column: "route_address_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_address_details_vehicle_enrollment_id", + schema: "application", + table: "route_address_details", + column: "vehicle_enrollment_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollments_route_id", + schema: "application", + table: "vehicle_enrollments", + column: "route_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollments_vehicle_id", + schema: "application", + table: "vehicle_enrollments", + column: "vehicle_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "route_address_details", + schema: "application"); + + migrationBuilder.DropTable( + name: "vehicle_enrollments", + schema: "application"); + + migrationBuilder.DropSequence( + name: "route_address_details_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicle_enrollments_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 8140f00..b480530 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -31,10 +31,14 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("regions_id_sequence"); + modelBuilder.HasSequence("route_address_details_id_sequence"); + modelBuilder.HasSequence("route_addresses_id_sequence"); modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + modelBuilder.HasSequence("vehicles_id_sequence"); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => @@ -264,6 +268,55 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("route_addresses", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => { b.Property("Id") @@ -299,6 +352,55 @@ namespace Persistence.PostgreSql.Migrations b.UseTphMappingStrategy(); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => { b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); @@ -443,6 +545,48 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Route"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Navigation("AddressRoutes"); @@ -466,6 +610,23 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => { b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); }); #pragma warning restore 612, 618 } diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index afede19..c9e667e 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -36,7 +36,16 @@ public class PostgreSqlDbContext : DbContext { builder .Properties() - .HaveColumnType("vehicle_type") + .HaveColumnType("varchar(16)") .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(8)") + .HaveConversion(); + + builder + .Properties() + .HaveConversion(); } } diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 0642a3e..0e588c2 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -22,6 +22,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork BusRepository = new PostgreSqlBusRepository(_dbContext); AircraftRepository = new PostgreSqlAircraftRepository(_dbContext); TrainRepository = new PostgreSqlTrainRepository(_dbContext); + VehicleEnrollmentRepository = + new PostgreSqlVehicleEnrollmentRepository(_dbContext); + RouteAddressRepository = + new PostgreSqlRouteAddressRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -42,6 +46,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public TrainRepository TrainRepository { get; init; } + public VehicleEnrollmentRepository VehicleEnrollmentRepository { get; init; } + + public RouteAddressRepository RouteAddressRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs new file mode 100644 index 0000000..fcd828b --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteAddressRepository : + PostgreSqlBaseRepository, RouteAddressRepository +{ + public PostgreSqlRouteAddressRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs new file mode 100644 index 0000000..434b69c --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleEnrollmentRepository : + PostgreSqlBaseRepository, VehicleEnrollmentRepository +{ + public PostgreSqlVehicleEnrollmentRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/TypeConverters/CurrencyConverter.cs b/src/Persistence/TypeConverters/CurrencyConverter.cs new file mode 100644 index 0000000..9c2bb7f --- /dev/null +++ b/src/Persistence/TypeConverters/CurrencyConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class CurrencyConverter : ValueConverter +{ + public CurrencyConverter() + : base( + v => v.Name, + v => Currency.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs b/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs new file mode 100644 index 0000000..841c33c --- /dev/null +++ b/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +// Convert localized time to UTC + +public class DateTimeOffsetConverter : + ValueConverter +{ + public DateTimeOffsetConverter() + : base( + v => v.ToUniversalTime(), + v => v) + { } +} From b1aceac75013afba82d22e89bc7932fd999f4359 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 14 May 2025 17:43:10 +0300 Subject: [PATCH 12/35] add currency converter service and integrated it with vehicle enrollment management --- .../Services/CurrencyConverterService.cs | 12 +++ .../Services/SessionCurrencyService.cs | 8 ++ .../GetVehicleEnrollmentQueryHandler.cs | 28 ++++- .../GetVehicleEnrollmentsPageQueryHandler.cs | 36 ++++++- src/Configuration/Identity/Configuration.cs | 3 + .../Infrastructure/Configuration.cs | 40 +++---- .../Persistence/Configuration.cs | 3 + src/Configuration/packages.lock.json | 40 ++++++- src/Domain/Enums/Currency.cs | 9 +- src/HttpApi/HttpApi.csproj | 2 +- src/HttpApi/Program.cs | 20 +++- .../Services/AspNetSessionCurrencyService.cs | 38 +++++++ .../AcceptCurrencyHeaderOperationFilter.cs | 39 +++++++ src/HttpApi/packages.lock.json | 62 ++++++----- src/Infrastructure/Infrastructure.csproj | 5 + .../ExchangeApiCurrencyConverterService.cs | 101 ++++++++++++++++++ src/Infrastructure/packages.lock.json | 76 +++++++++++++ .../packages.lock.json | 39 +++++-- 18 files changed, 493 insertions(+), 68 deletions(-) create mode 100644 src/Application/Common/Interfaces/Services/CurrencyConverterService.cs create mode 100644 src/Application/Common/Interfaces/Services/SessionCurrencyService.cs create mode 100644 src/HttpApi/Services/AspNetSessionCurrencyService.cs create mode 100644 src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs create mode 100644 src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs diff --git a/src/Application/Common/Interfaces/Services/CurrencyConverterService.cs b/src/Application/Common/Interfaces/Services/CurrencyConverterService.cs new file mode 100644 index 0000000..4f47c30 --- /dev/null +++ b/src/Application/Common/Interfaces/Services/CurrencyConverterService.cs @@ -0,0 +1,12 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +public interface CurrencyConverterService +{ + Task ConvertAsync(decimal amount, Currency from, Currency to, + CancellationToken cancellationToken); + + Task ConvertAsync(decimal amount, Currency from, Currency to, + DateTimeOffset time, CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Interfaces/Services/SessionCurrencyService.cs b/src/Application/Common/Interfaces/Services/SessionCurrencyService.cs new file mode 100644 index 0000000..258a3c0 --- /dev/null +++ b/src/Application/Common/Interfaces/Services/SessionCurrencyService.cs @@ -0,0 +1,8 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +public interface SessionCurrencyService +{ + public Currency Currency { get; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs index 5ca9b2c..5cb3804 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs @@ -2,6 +2,8 @@ using MediatR; using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments .Queries.GetVehicleEnrollment; @@ -12,12 +14,19 @@ public class GetVehicleEnrollmentQueryHandler : private readonly UnitOfWork _unitOfWork; private readonly IMapper _mapper; + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + public GetVehicleEnrollmentQueryHandler( UnitOfWork unitOfWork, - IMapper mapper) + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) { _unitOfWork = unitOfWork; _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; } public async Task Handle( @@ -51,6 +60,23 @@ public class GetVehicleEnrollmentQueryHandler : .First(ra => ra.Id == rad.RouteAddressId); } + + // Convert currency + + // TODO: Replace with AutoMapper Resolver + + if (!_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + foreach (var rad in entity.RouteAddressDetails) + { + rad.CostToNextAddress = await _currencyConverterService + .ConvertAsync(rad.CostToNextAddress, entity.Currency, + _sessionCurrencyService.Currency, cancellationToken); + } + entity.Currency = _sessionCurrencyService.Currency; + } + + _unitOfWork.Dispose(); return _mapper.Map(entity); diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs index ee4f86c..114aa9d 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs @@ -3,6 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments .Queries.GetVehicleEnrollmentsPage; @@ -13,12 +15,19 @@ public class GetVehicleEnrollmentsPageQueryHandler : private readonly UnitOfWork _unitOfWork; private readonly IMapper _mapper; + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + public GetVehicleEnrollmentsPageQueryHandler( UnitOfWork unitOfWork, - IMapper mapper) + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) { _unitOfWork = unitOfWork; _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; } public async Task> Handle( @@ -55,13 +64,15 @@ public class GetVehicleEnrollmentsPageQueryHandler : (request.ArrivalTimeGreaterThanOrEqual != null ? e.DepartureTime.AddSeconds(e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) >= + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) >= request.ArrivalTimeGreaterThanOrEqual : true) && (request.ArrivalTimeLessThanOrEqual != null ? e.DepartureTime.AddSeconds(e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) <= + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) <= request.ArrivalTimeLessThanOrEqual : true) && (request.TravelTimeGreaterThanOrEqual != null @@ -141,6 +152,25 @@ public class GetVehicleEnrollmentsPageQueryHandler : } + // Convert currency + + // TODO: Replace with AutoMapper Resolver + + if (!_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + foreach (var ve in paginatedList.Items) + { + foreach (var rad in ve.RouteAddressDetails) + { + rad.CostToNextAddress = await _currencyConverterService + .ConvertAsync(rad.CostToNextAddress, ve.Currency, + _sessionCurrencyService.Currency, cancellationToken); + } + ve.Currency = _sessionCurrencyService.Currency; + } + } + + var mappedItems = _mapper .ProjectTo(paginatedList.Items.AsQueryable()); diff --git a/src/Configuration/Identity/Configuration.cs b/src/Configuration/Identity/Configuration.cs index d066380..0aba448 100644 --- a/src/Configuration/Identity/Configuration.cs +++ b/src/Configuration/Identity/Configuration.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using cuqmbr.TravelGuide.Identity.Exceptions; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace cuqmbr.TravelGuide.Configuration.Identity; @@ -38,6 +39,8 @@ public static class Configuration "ef_migrations_history", configuration.Datastore.PartitionName); }); + options.ConfigureWarnings(w => w.Ignore( + RelationalEventId.PendingModelChangesWarning)); }); services diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index ebbe6ce..7e8793c 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -1,19 +1,21 @@ -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.DependencyInjection; -// -// namespace cuqmbr.TravelGuide.Configuration.Infrastructure; -// -// public static class Configuration -// { -// public static IServiceCollection ConfigureInfrastructure( -// this IServiceCollection services, -// IConfiguration configuration) -// { -// services -// .AddIdentity(configuration) -// .AddAuthenticationWithJwt(configuration) -// .AddServices(); -// -// return services; -// } -// } +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace cuqmbr.TravelGuide.Configuration.Infrastructure; + +public static class Configuration +{ + public static IServiceCollection ConfigureInfrastructure( + this IServiceCollection services) + { + services + .AddHttpClient(); + + services + .AddScoped< + CurrencyConverterService, + ExchangeApiCurrencyConverterService>(); + + return services; + } +} diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 396aeba..a506520 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -6,6 +6,7 @@ using cuqmbr.TravelGuide.Persistence.Exceptions; using Microsoft.EntityFrameworkCore; using cuqmbr.TravelGuide.Persistence.PostgreSql; using cuqmbr.TravelGuide.Persistence.InMemory; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace cuqmbr.TravelGuide.Configuration.Persistence; @@ -34,6 +35,8 @@ public static class Configuration "ef_migrations_history", configuration.PartitionName); }); + options.ConfigureWarnings(w => w.Ignore( + RelationalEventId.PendingModelChangesWarning)); }); services diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index 7bd3fb8..47118f9 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -495,13 +495,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -539,6 +549,19 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", "resolved": "9.0.4", @@ -696,6 +719,11 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -832,7 +860,9 @@ "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { diff --git a/src/Domain/Enums/Currency.cs b/src/Domain/Enums/Currency.cs index 4960808..fb5f312 100644 --- a/src/Domain/Enums/Currency.cs +++ b/src/Domain/Enums/Currency.cs @@ -3,16 +3,23 @@ namespace cuqmbr.TravelGuide.Domain.Enums; // Do not forget to update the schema of your database when changing // this class (if you use it with a database) -// ISO-4217 Country Codes dated 2025-03-31 +// ISO-4217 Currency Codes dated 2025-03-31 public abstract class Currency : Enumeration { + public static readonly Currency Default = new DefaultCurrency(); public static readonly Currency USD = new USDCurrency(); public static readonly Currency EUR = new EURCurrency(); public static readonly Currency UAH = new UAHCurrency(); protected Currency(int value, string name) : base(value, name) { } + // When no currency is specified + private sealed class DefaultCurrency : Currency + { + public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { } + } + private sealed class USDCurrency : Currency { public USDCurrency() : base(840, "USD") { } diff --git a/src/HttpApi/HttpApi.csproj b/src/HttpApi/HttpApi.csproj index 8afd0fb..b85d0a5 100644 --- a/src/HttpApi/HttpApi.csproj +++ b/src/HttpApi/HttpApi.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index 9f72a50..28b4764 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -1,5 +1,6 @@ using cuqmbr.TravelGuide.Configuration.Persistence; using cuqmbr.TravelGuide.Configuration.Application; +using cuqmbr.TravelGuide.Configuration.Infrastructure; using cuqmbr.TravelGuide.Configuration.Identity; using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; @@ -9,7 +10,7 @@ using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; using System.Net; using Swashbuckle.AspNetCore.SwaggerUI; -using MicroElements.Swashbuckle.FluentValidation.AspNetCore; +// using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.OpenApi.Models; using System.Reflection; @@ -25,12 +26,13 @@ services.ConfigureLogging(); services.ConfigurePersistence(); services.ConfigureIdentity(); -// services.AddInfrastructure(); +services.ConfigureInfrastructure(); services.ConfigureApplication(); services.AddScoped(); -services.AddScoped(); services.AddScoped(); +services.AddScoped(); +services.AddScoped(); services.AddControllers(); @@ -82,8 +84,18 @@ services.AddSwaggerGen(options => "from IANA tz database (https://www.iana.org/time-zones).", Type = SecuritySchemeType.ApiKey }); + + // Set Accept-Currency header in Authorize window + options.OperationFilter(); + options.AddSecurityDefinition("Accept-Currency", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Accept-Currency", + Description = "ISO-4217 Currency Code.", + Type = SecuritySchemeType.ApiKey + }); }); -services.AddFluentValidationRulesToSwagger(); +// services.AddFluentValidationRulesToSwagger(); services.AddScoped(); diff --git a/src/HttpApi/Services/AspNetSessionCurrencyService.cs b/src/HttpApi/Services/AspNetSessionCurrencyService.cs new file mode 100644 index 0000000..8b28990 --- /dev/null +++ b/src/HttpApi/Services/AspNetSessionCurrencyService.cs @@ -0,0 +1,38 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.HttpApi.Services; + +public sealed class AspNetSessionCurrencyService : SessionCurrencyService +{ + private readonly HttpContext _httpContext; + + public AspNetSessionCurrencyService(IHttpContextAccessor httpContextAccessor) + { + _httpContext = httpContextAccessor.HttpContext; + } + + public Currency Currency + { + get + { + string? headerCurrencyCode = + _httpContext.Request.Headers["Accept-Currency"]; + + string currencyCode = + headerCurrencyCode?.ToUpper() ?? + cuqmbr.TravelGuide.Domain.Enums.Currency.Default.Name; + + var resultCurrency = + cuqmbr.TravelGuide.Domain.Enums.Currency.FromName(currencyCode); + + if (resultCurrency == null) + { + resultCurrency = + cuqmbr.TravelGuide.Domain.Enums.Currency.Default; + } + + return resultCurrency; + } + } +} diff --git a/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs b/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs new file mode 100644 index 0000000..a661bfa --- /dev/null +++ b/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs @@ -0,0 +1,39 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; + +public class AcceptCurrencyHeaderOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // TODO: Remove security requirements + operation.Security ??= new List(); + + var acceptCurrency = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Accept-Currency" + } + }; + + operation.Security.Add(new OpenApiSecurityRequirement + { + [acceptCurrency] = new List() + }); + + if (operation.Parameters == null) + operation.Parameters = new List(); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Accept-Currency", + Description = "ISO-4217 Currency Code.", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = "String" }, + Required = false + }); + } +} diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index f7cac22..a8c38fc 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -13,17 +13,6 @@ "Microsoft.Extensions.Localization": "9.0.0" } }, - "MicroElements.Swashbuckle.FluentValidation": { - "type": "Direct", - "requested": "[6.1.0, )", - "resolved": "6.1.0", - "contentHash": "VzqApLPY8xIqXDvWqRuvoDYEoCHII42c4LgvLO3BikKoIVcECD+ZSG727I7yPZ/J07VEoa8aJddoqUtSm4E4gw==", - "dependencies": { - "FluentValidation": "[10.0.0, 12.0.0)", - "MicroElements.OpenApi.FluentValidation": "6.1.0", - "Swashbuckle.AspNetCore.SwaggerGen": "[6.3.0, 8.0.0)" - } - }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", "requested": "[9.0.4, )", @@ -159,17 +148,6 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, - "MicroElements.OpenApi.FluentValidation": { - "type": "Transitive", - "resolved": "6.1.0", - "contentHash": "qJPAI3bL70ND6fIi4bGqQf/lpV9wUod23R7JVhVVYRonoT/ZGmPBjMbO//IPPy3yP2F21iMX05brYjyU4/WqwQ==", - "dependencies": { - "FluentValidation": "[10.0.0, 12.0.0)", - "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, "Microsoft.AspNetCore.Authentication": { "type": "Transitive", "resolved": "2.3.0", @@ -634,13 +612,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -678,6 +666,19 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", "resolved": "9.0.4", @@ -873,6 +874,11 @@ "System.CodeDom": "6.0.0" } }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -1119,7 +1125,9 @@ "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index d704cae..2ae6a29 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -10,6 +10,11 @@ + + + + + true diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs new file mode 100644 index 0000000..15d444d --- /dev/null +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using Newtonsoft.Json; + +// https://github.com/fawazahmed0/exchange-api + +public sealed class ExchangeApiCurrencyConverterService : + CurrencyConverterService +{ + private readonly + IDictionary< + DateOnly, IDictionary< + Currency, IDictionary< + Currency, decimal>>> _cache; + + private const string urlFormat = "https://cdn.jsdelivr.net/" + + "npm/@fawazahmed0/currency-api@{0}/v1/currencies/{1}.json"; + + private readonly IHttpClientFactory _httpClientFactory; + + public ExchangeApiCurrencyConverterService( + IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + + _cache = + new Dictionary< + DateOnly, IDictionary< + Currency, IDictionary< + Currency, decimal>>>(); + } + + public async Task ConvertAsync(decimal amount, Currency from, + Currency to, CancellationToken cancellationToken) + { + return await ConvertAsync(amount, from, to, + DateTimeOffset.UtcNow, cancellationToken); + } + + public async Task ConvertAsync(decimal amount, Currency from, + Currency to, DateTimeOffset time, CancellationToken cancellationToken) + { + if (from.Equals(to)) + { + return amount; + } + + var requestDate = DateOnly.FromDateTime(time.ToUniversalTime().Date); + + // Return cached value if available + if (_cache.Keys.Contains(requestDate) && + _cache[requestDate].Keys.Contains(from) && + _cache[requestDate][from].Keys.Contains(to)) + { + return amount * _cache[requestDate][from][to]; + } + + + // Retreive new value from api + + var httpClient = _httpClientFactory.CreateClient(); + + var requestDateString = + requestDate == DateOnly.FromDateTime(DateTime.UtcNow) ? + "latest" : + requestDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + var url = String.Format(urlFormat, + requestDateString, from.Name.ToLower()); + + var jsonString = + await httpClient.GetStringAsync(url, cancellationToken); + + var obj = JsonConvert.DeserializeObject(jsonString); + + decimal rate = obj[from.Name.ToLower()][to.Name.ToLower()]; + + + // Cache new value + + if (!_cache.Keys.Contains(requestDate)) + { + _cache.Add(requestDate, + new Dictionary>()); + } + + if (!_cache[requestDate].Keys.Contains(from)) + { + _cache[requestDate].Add(from, new Dictionary()); + } + + if (!_cache[requestDate][from].Keys.Contains(to)) + { + _cache[requestDate][from].Add(to, rate); + } + + + return amount * rate; + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index 83c6755..b89053f 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -2,6 +2,26 @@ "version": 1, "dependencies": { "net9.0": { + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "AspNetCore.Localizer.Json": { "type": "Transitive", "resolved": "1.0.1", @@ -97,6 +117,31 @@ "Microsoft.Extensions.Primitives": "9.0.0" } }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "9.0.4", @@ -110,6 +155,25 @@ "resolved": "9.0.4", "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, "Microsoft.Extensions.Localization": { "type": "Transitive", "resolved": "9.0.0", @@ -153,6 +217,18 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "9.0.4", diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 017ffbc..4fd295d 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -497,13 +497,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -541,6 +551,19 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", "resolved": "9.0.4", @@ -772,8 +795,8 @@ }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "13.0.1", - "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Npgsql": { "type": "Transitive", @@ -1005,7 +1028,9 @@ "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { From 74dc7ceff3d0fc767ffba9d901dce235ac76877e Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 14 May 2025 17:48:48 +0300 Subject: [PATCH 13/35] rename user setings services --- .../Commands/AddAddress/AddAddressCommandValidator.cs | 2 +- .../UpdateAddress/UpdateAddressCommandValidator.cs | 2 +- .../GetAddressesPage/GetAddressesPageQueryValidator.cs | 2 +- .../Commands/AddAircraft/AddAircraftCommandValidator.cs | 2 +- .../UpdateAircraft/UpdateAircraftCommandValidator.cs | 2 +- .../GetAircraftsPage/GetAircraftsPageQueryValidator.cs | 2 +- .../Buses/Commands/AddBus/AddBusCommandValidator.cs | 2 +- .../Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs | 2 +- .../Queries/GetBusesPage/GetBusesPageQueryValidator.cs | 2 +- .../Cities/Commands/AddCity/AddCityCommandValidator.cs | 2 +- .../Commands/UpdateCity/UpdateCityCommandValidator.cs | 2 +- .../Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs | 2 +- .../{TimeZoneService.cs => SessionTimeZoneService.cs} | 2 +- .../{CultureService.cs => SessoionCultureService.cs} | 2 +- .../Common/Mappings/Resolvers/DateTimeOffsetResolver.cs | 4 ++-- .../Commands/AddCountry/AddCountryCommandValidator.cs | 2 +- .../UpdateCountry/UpdateCountryCommandValidator.cs | 2 +- .../GetCountriesPage/GetCountriesPageQueryValidator.cs | 2 +- .../Commands/AddRegion/AddRegionCommandValidator.cs | 2 +- .../Commands/UpdateRegion/UpdateRegionCommandValidator.cs | 2 +- .../GetRegionsPage/GetRegionsPageQueryValidator.cs | 2 +- .../Routes/Commands/AddRoute/AddRouteCommandValidator.cs | 2 +- .../Commands/UpdateRoute/UpdateRouteCommandValidator.cs | 2 +- .../Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs | 2 +- .../Trains/Commands/AddTrain/AddTrainCommandValidator.cs | 2 +- .../Commands/UpdateTrain/UpdateTrainCommandValidator.cs | 2 +- .../Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs | 2 +- .../AddVehicleEnrollmentCommandValidator.cs | 4 ++-- .../UpdateVehicleEnrollmentCommandValidator.cs | 4 ++-- .../GetVehicleEnrollmentsPageQueryValidator.cs | 2 +- src/HttpApi/Controllers/TestsController.cs | 2 +- src/HttpApi/HttpApi.csproj | 1 - src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs | 4 ++-- src/HttpApi/Program.cs | 6 ++---- ...etCultureService.cs => AspNetSessionCultureService.cs} | 4 ++-- ...TimeZoneService.cs => AspNetSessionTimeZoneService.cs} | 4 ++-- tst/Application.IntegrationTests/BaseTest.cs | 8 ++++---- 37 files changed, 46 insertions(+), 49 deletions(-) rename src/Application/Common/Interfaces/Services/{TimeZoneService.cs => SessionTimeZoneService.cs} (74%) rename src/Application/Common/Interfaces/Services/{CultureService.cs => SessoionCultureService.cs} (78%) rename src/HttpApi/Services/{AspNetCultureService.cs => AspNetSessionCultureService.cs} (87%) rename src/HttpApi/Services/{AspNetTimeZoneService.cs => AspNetSessionTimeZoneService.cs} (79%) diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs index 2ca5840..59adff1 100644 --- a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs @@ -9,7 +9,7 @@ public class AddAddressCommandValidator : AbstractValidator { public AddAddressCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs index 59770c9..ceae48d 100644 --- a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateAddressCommandValidator : AbstractValidator v.Guid) .NotEmpty() diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs index af43f25..b1eb480 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetAddressesPageQueryValidator : AbstractValidator v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs index 76f5fe1..617c394 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -8,7 +8,7 @@ public class AddAircraftCommandValidator : AbstractValidator { public AddAircraftCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Number) .NotEmpty() diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs index 70fb15a..c33cecc 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateAircraftCommandValidator : AbstractValidator v.Guid) .NotEmpty() diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs index bed93b0..49c58b3 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetAircraftsPageQueryValidator : AbstractValidator v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs index 46bf658..1ce4caa 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -8,7 +8,7 @@ public class AddBusCommandValidator : AbstractValidator { public AddBusCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Number) .NotEmpty() diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs index 7097ab2..4224d6f 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateBusCommandValidator : AbstractValidator { public UpdateBusCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Guid) .NotEmpty() diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs index f72c75e..7ab1118 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetBusesPageQueryValidator : AbstractValidator { public GetBusesPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs index 2a0a931..6b20606 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -8,7 +8,7 @@ public class AddCityCommandValidator : AbstractValidator { public AddCityCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs index b813ca2..bdc49c0 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateCityCommandValidator : AbstractValidator { public UpdateCityCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Guid) .NotEmpty() diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs index dde8a20..53e5264 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetCitiesPageQueryValidator : AbstractValidator { public GetCitiesPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Common/Interfaces/Services/TimeZoneService.cs b/src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs similarity index 74% rename from src/Application/Common/Interfaces/Services/TimeZoneService.cs rename to src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs index 6ec775c..ed60d8b 100644 --- a/src/Application/Common/Interfaces/Services/TimeZoneService.cs +++ b/src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs @@ -1,6 +1,6 @@ namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -public interface TimeZoneService +public interface SessionTimeZoneService { public TimeZoneInfo TimeZone { get; } } diff --git a/src/Application/Common/Interfaces/Services/CultureService.cs b/src/Application/Common/Interfaces/Services/SessoionCultureService.cs similarity index 78% rename from src/Application/Common/Interfaces/Services/CultureService.cs rename to src/Application/Common/Interfaces/Services/SessoionCultureService.cs index 3ee1ee8..23d7389 100644 --- a/src/Application/Common/Interfaces/Services/CultureService.cs +++ b/src/Application/Common/Interfaces/Services/SessoionCultureService.cs @@ -2,7 +2,7 @@ using System.Globalization; namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -public interface CultureService +public interface SessionCultureService { public CultureInfo Culture { get; } } diff --git a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs index 679eb28..56423e8 100644 --- a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs +++ b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs @@ -6,9 +6,9 @@ namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; public class DateTimeOffsetToLocalResolver : IMemberValueResolver { - private readonly TimeZoneService _timeZoneService; + private readonly SessionTimeZoneService _timeZoneService; - public DateTimeOffsetToLocalResolver(TimeZoneService timeZoneService) + public DateTimeOffsetToLocalResolver(SessionTimeZoneService timeZoneService) { _timeZoneService = timeZoneService; } diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs index 51e03ce..ec854f3 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs @@ -8,7 +8,7 @@ public class AddCountryCommandValidator : AbstractValidator { public AddCountryCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs index d2d0612..6809cb9 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateCountryCommandValidator : AbstractValidator v.Guid) .NotEmpty() diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs index 59d2866..ceeea99 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs @@ -9,7 +9,7 @@ public class GetCountriesPageQueryValidator : { public GetCountriesPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs index c35227d..9d3de32 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs @@ -8,7 +8,7 @@ public class AddRegionCommandValidator : AbstractValidator { public AddRegionCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs index 6cb3dfb..4366378 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateRegionCommandValidator : AbstractValidator v.Guid) .NotEmpty() diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs index e075ef5..b064c3d 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetRegionsPageQueryValidator : AbstractValidator v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs index ead366b..79fec33 100644 --- a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs @@ -9,7 +9,7 @@ public class AddRouteCommandValidator : AbstractValidator { public AddRouteCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs index cc4ddd3..60b8485 100644 --- a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs @@ -9,7 +9,7 @@ public class UpdateRouteCommandValidator : AbstractValidator { public UpdateRouteCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Guid) .NotEmpty() diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs index 3ad2ba7..e38376d 100644 --- a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs @@ -9,7 +9,7 @@ public class GetRoutesPageQueryValidator : AbstractValidator { public GetRoutesPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs index f8797c5..ec530dd 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -8,7 +8,7 @@ public class AddTrainCommandValidator : AbstractValidator { public AddTrainCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Number) .NotEmpty() diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs index 689d2e8..b39eac1 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -8,7 +8,7 @@ public class UpdateTrainCommandValidator : AbstractValidator { public UpdateTrainCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Guid) .NotEmpty() diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs index 1b56968..a4d9410 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetTrainsPageQueryValidator : AbstractValidator { public GetTrainsPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs index c971787..c89f266 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs @@ -11,8 +11,8 @@ public class AddVehicleEnrollmentCommandValidator : { public AddVehicleEnrollmentCommandValidator( IStringLocalizer localizer, - CultureService cultureService, - TimeZoneService timeZoneService) + SessionCultureService cultureService, + SessionTimeZoneService timeZoneService) { RuleFor(v => v.DepartureTime) .NotEmpty() diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs index c15552f..1cd8966 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs @@ -11,8 +11,8 @@ public class UpdateVehicleEnrollmentCommandValidator : { public UpdateVehicleEnrollmentCommandValidator( IStringLocalizer localizer, - CultureService cultureService, - TimeZoneService timeZoneService) + SessionCultureService cultureService, + SessionTimeZoneService timeZoneService) { RuleFor(v => v.Guid) .NotEmpty() diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs index 9cfb2d3..5e0ba41 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs @@ -8,7 +8,7 @@ public class GetVehicleEnrollmentsPageQueryValidator : AbstractValidator v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index b75627a..be91e8b 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -12,7 +12,7 @@ public class TestsController : ControllerBase private readonly UnitOfWork _unitOfWork; public TestsController( - CultureService cultureService, + SessionCultureService cultureService, IStringLocalizer localizer, UnitOfWork unitOfWork) { diff --git a/src/HttpApi/HttpApi.csproj b/src/HttpApi/HttpApi.csproj index b85d0a5..5719f8b 100644 --- a/src/HttpApi/HttpApi.csproj +++ b/src/HttpApi/HttpApi.csproj @@ -13,7 +13,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs index 8b02796..3f245fb 100644 --- a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs +++ b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs @@ -5,9 +5,9 @@ namespace cuqmbr.TravelGuide.HttpApi.Middlewares; public class ThreadCultureSetterMiddleware : IMiddleware { - private readonly CultureService _cultureService; + private readonly SessionCultureService _cultureService; - public ThreadCultureSetterMiddleware(CultureService cultureService) + public ThreadCultureSetterMiddleware(SessionCultureService cultureService) { _cultureService = cultureService; } diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index 28b4764..472d41e 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -10,7 +10,6 @@ using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; using System.Net; using Swashbuckle.AspNetCore.SwaggerUI; -// using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.OpenApi.Models; using System.Reflection; @@ -30,8 +29,8 @@ services.ConfigureInfrastructure(); services.ConfigureApplication(); services.AddScoped(); -services.AddScoped(); -services.AddScoped(); +services.AddScoped(); +services.AddScoped(); services.AddScoped(); services.AddControllers(); @@ -95,7 +94,6 @@ services.AddSwaggerGen(options => Type = SecuritySchemeType.ApiKey }); }); -// services.AddFluentValidationRulesToSwagger(); services.AddScoped(); diff --git a/src/HttpApi/Services/AspNetCultureService.cs b/src/HttpApi/Services/AspNetSessionCultureService.cs similarity index 87% rename from src/HttpApi/Services/AspNetCultureService.cs rename to src/HttpApi/Services/AspNetSessionCultureService.cs index 03388ab..1c4ef7e 100644 --- a/src/HttpApi/Services/AspNetCultureService.cs +++ b/src/HttpApi/Services/AspNetSessionCultureService.cs @@ -3,12 +3,12 @@ using System.Globalization; namespace cuqmbr.TravelGuide.HttpApi.Services; -public sealed class AspNetCultureService : CultureService +public sealed class AspNetSessionCultureService : SessionCultureService { private readonly HttpContext _httpContext; private const string DefaultCultureId = "en-US"; - public AspNetCultureService(IHttpContextAccessor httpContextAccessor) + public AspNetSessionCultureService(IHttpContextAccessor httpContextAccessor) { _httpContext = httpContextAccessor.HttpContext!; } diff --git a/src/HttpApi/Services/AspNetTimeZoneService.cs b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs similarity index 79% rename from src/HttpApi/Services/AspNetTimeZoneService.cs rename to src/HttpApi/Services/AspNetSessionTimeZoneService.cs index eddcfb9..9feb264 100644 --- a/src/HttpApi/Services/AspNetTimeZoneService.cs +++ b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs @@ -2,11 +2,11 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; namespace cuqmbr.TravelGuide.HttpApi.Services; -public sealed class AspNetTimeZoneService : TimeZoneService +public sealed class AspNetSessionTimeZoneService : SessionTimeZoneService { private readonly HttpContext _httpContext; - public AspNetTimeZoneService(IHttpContextAccessor httpContextAccessor) + public AspNetSessionTimeZoneService(IHttpContextAccessor httpContextAccessor) { _httpContext = httpContextAccessor.HttpContext!; } diff --git a/tst/Application.IntegrationTests/BaseTest.cs b/tst/Application.IntegrationTests/BaseTest.cs index c919add..194ab99 100644 --- a/tst/Application.IntegrationTests/BaseTest.cs +++ b/tst/Application.IntegrationTests/BaseTest.cs @@ -88,9 +88,9 @@ public abstract class TestBase : IDisposable var cultureInfo = CultureInfo.GetCultureInfo(culture); _serviceCollection - .AddScoped(_ => + .AddScoped(_ => { - var mock = new Mock(); + var mock = new Mock(); mock .Setup(s => s.Culture) @@ -106,9 +106,9 @@ public abstract class TestBase : IDisposable public void SetTimeZone(string timeZone) { _serviceCollection - .AddScoped(_ => + .AddScoped(_ => { - var mock = new Mock(); + var mock = new Mock(); mock .Setup(s => s.TimeZone) From f4611f029ffc7e09dbc871e1f4d480c1f4c868a2 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 15 May 2025 19:18:52 +0300 Subject: [PATCH 14/35] add companies management --- src/Application/Aircrafts/AircraftDto.cs | 7 +- .../AddAircraft/AddAircraftCommand.cs | 2 + .../AddAircraft/AddAircraftCommandHandler.cs | 14 +- .../AddAircraftCommandValidator.cs | 4 + .../UpdateAircraft/UpdateAircraftCommand.cs | 2 + .../UpdateAircraftCommandHandler.cs | 13 +- .../UpdateAircraftCommandValidator.cs | 4 + .../GetAircraft/GetAircraftQueryHandler.cs | 3 +- .../GetAircraftsPage/GetAircraftsPageQuery.cs | 8 +- .../GetAircraftsPageQueryHandler.cs | 12 +- .../ViewModels/AddAircraftViewModel.cs | 2 + .../GetAircraftsPageFilterViewModel.cs | 8 +- .../ViewModels/UpdateAircraftViewModel.cs | 2 + src/Application/Buses/BusDto.cs | 7 +- .../Buses/Commands/AddBus/AddBusCommand.cs | 2 + .../Commands/AddBus/AddBusCommandHandler.cs | 13 +- .../Commands/AddBus/AddBusCommandValidator.cs | 4 + .../Commands/UpdateBus/UpdateBusCommand.cs | 2 + .../UpdateBus/UpdateBusCommandHandler.cs | 13 +- .../UpdateBus/UpdateBusCommandValidator.cs | 4 + .../Queries/GetBus/GetBusQueryHandler.cs | 3 +- .../Queries/GetBusesPage/GetBusesPageQuery.cs | 8 +- .../GetBusesPage/GetBusesPageQueryHandler.cs | 12 +- .../Buses/ViewModels/AddBusViewModel.cs | 2 + .../ViewModels/GetBusesPageFilterViewModel.cs | 11 +- .../Buses/ViewModels/UpdateBusViewModel.cs | 2 + .../FluentValidation/CustomValidators.cs | 24 + .../Repositories/CompanyRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../Commands/AddCompany/AddCompanyCommand.cs | 14 + .../AddCompany/AddCompanyCommandAuthorizer.cs | 31 + .../AddCompany/AddCompanyCommandHandler.cs | 52 ++ .../AddCompany/AddCompanyCommandValidator.cs | 58 ++ .../DeleteCompany/DeleteCompanyCommand.cs | 8 + .../DeleteCompanyCommandAuthorizer.cs | 31 + .../DeleteCompanyCommandHandler.cs | 34 + .../DeleteCompanyCommandValidator.cs | 14 + .../UpdateCompany/UpdateCompanyCommand.cs | 16 + .../UpdateCompanyCommandAuthorizer.cs | 31 + .../UpdateCompanyCommandHandler.cs | 47 ++ .../UpdateCompanyCommandValidator.cs | 62 ++ src/Application/Companies/CompanyDto.cs | 25 + .../GetCompaniesPage/GetCompaniesPageQuery.cs | 15 + .../GetCompaniesPageQueryAuthorizer.cs | 31 + .../GetCompaniesPageQueryHandler.cs | 49 ++ .../GetCompaniesPageQueryValidator.cs | 43 ++ .../Queries/GetCompany/GetCompanyQuery.cs | 8 + .../GetCompany/GetCompanyQueryAuthorizer.cs | 31 + .../GetCompany/GetCompanyQueryHandler.cs | 38 + .../GetCompany/GetCompanyQueryValidator.cs | 14 + .../ViewModels/AddCompanyViewModel.cs | 12 + .../ViewModels/UpdateCompanyViewModel.cs | 12 + .../Resources/Localization/en-US.json | 4 +- .../Commands/AddTrain/AddTrainCommand.cs | 2 + .../AddTrain/AddTrainCommandHandler.cs | 13 +- .../AddTrain/AddTrainCommandValidator.cs | 4 + .../UpdateTrain/UpdateTrainCommand.cs | 2 + .../UpdateTrain/UpdateTrainCommandHandler.cs | 13 +- .../UpdateTrainCommandValidator.cs | 4 + .../Queries/GetTrain/GetTrainQueryHandler.cs | 3 +- .../GetTrainsPage/GetTrainsPageQuery.cs | 4 +- .../GetTrainsPageQueryHandler.cs | 4 + src/Application/Trains/TrainDto.cs | 7 +- .../Trains/ViewModels/AddTrainViewModel.cs | 2 + .../GetTrainsPageFilterViewModel.cs | 10 +- .../Trains/ViewModels/UpdateTrainViewModel.cs | 2 + src/Domain/Entities/Company.cs | 15 + src/Domain/Entities/Vehicle.cs | 5 + .../Controllers/AircraftsController.cs | 16 +- src/HttpApi/Controllers/BusesController.cs | 16 +- .../Controllers/CompaniesController.cs | 190 +++++ src/HttpApi/Controllers/TrainsController.cs | 4 +- .../InMemory/InMemoryUnitOfWork.cs | 3 + .../Repositories/InMemoryCompanyRepository.cs | 11 + .../Configurations/CompanyConfiguration.cs | 42 ++ .../Configurations/VehicleConfiguration.cs | 24 + .../20250515101417_Add_Companies.Designer.cs | 706 ++++++++++++++++++ .../20250515101417_Add_Companies.cs | 110 +++ .../PostgreSqlDbContextModelSnapshot.cs | 71 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 3 + .../PostgreSqlCompanyRepository.cs | 11 + 81 files changed, 2103 insertions(+), 70 deletions(-) create mode 100644 src/Application/Common/FluentValidation/CustomValidators.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs create mode 100644 src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs create mode 100644 src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs create mode 100644 src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs create mode 100644 src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs create mode 100644 src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs create mode 100644 src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs create mode 100644 src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs create mode 100644 src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs create mode 100644 src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs create mode 100644 src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs create mode 100644 src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs create mode 100644 src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs create mode 100644 src/Application/Companies/CompanyDto.cs create mode 100644 src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs create mode 100644 src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs create mode 100644 src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs create mode 100644 src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs create mode 100644 src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs create mode 100644 src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs create mode 100644 src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs create mode 100644 src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs create mode 100644 src/Application/Companies/ViewModels/AddCompanyViewModel.cs create mode 100644 src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs create mode 100644 src/Domain/Entities/Company.cs create mode 100644 src/HttpApi/Controllers/CompaniesController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs index 8549160..58c8866 100644 --- a/src/Application/Aircrafts/AircraftDto.cs +++ b/src/Application/Aircrafts/AircraftDto.cs @@ -13,11 +13,16 @@ public sealed class AircraftDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs index f938a36..3e3a9ff 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs @@ -9,4 +9,6 @@ public record AddAircraftCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs index 8ffe4e7..a210946 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -33,11 +33,23 @@ public class AddAircraftCommandHandler : "Aircraft with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + + entity = new Aircraft() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.AircraftRepository.AddOneAsync( diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs index 617c394..b37dbac 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -33,5 +33,9 @@ public class AddAircraftCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs index 77a174e..0de5717 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs @@ -11,4 +11,6 @@ public record UpdateAircraftCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs index 952af00..d046aa9 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateAircraftCommandHandler : } var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateAircraftCommandHandler : "Aircraft with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.AircraftRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs index c33cecc..fb9dbc8 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateAircraftCommandValidator : AbstractValidator v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs index a92c658..01e7454 100644 --- a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -24,7 +24,8 @@ public class GetAircraftQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.AircraftRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs index df99a61..655a576 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs @@ -13,11 +13,9 @@ public record GetAircraftsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } + public Guid? CompanyGuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityGreaterOrEqualThan { get; set; } - - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs index e75b5f0..e4a59c3 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetAircraftsPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && - (request.CapacityGreaterOrEqualThan != null - ? e.Capacity >= request.CapacityGreaterOrEqualThan + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid : true) && - (request.CapacityLessOrEqualThan != null - ? e.Capacity <= request.CapacityLessOrEqualThan + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs index 3a6e5bd..57b64e0 100644 --- a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddAircraftViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs index c956d2b..51994b5 100644 --- a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs @@ -2,14 +2,12 @@ namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; public sealed class GetAircraftsPageFilterViewModel { - public string? Number { get; set; } - - public string? Model { get; set; } + public Guid? CompanyUuid { get; set; } // TODO: Consider adding strict equals rule although it is not // necessarily needed to filter with exact capacity - public short? CapacityGreaterOrEqualThan { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs index 1c4e71e..52e9374 100644 --- a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateAircraftViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs index 3ba7ae5..1be10a6 100644 --- a/src/Application/Buses/BusDto.cs +++ b/src/Application/Buses/BusDto.cs @@ -13,11 +13,16 @@ public sealed class BusDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs index 786a773..00f0405 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs @@ -9,4 +9,6 @@ public record AddBusCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs index c5a8488..346bd4b 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -33,11 +33,22 @@ public class AddBusCommandHandler : "Bus with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity = new Bus() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.BusRepository.AddOneAsync( diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs index 1ce4caa..084cfcc 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -33,5 +33,9 @@ public class AddBusCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs index 9754c7a..4a5f18a 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs @@ -11,4 +11,6 @@ public record UpdateBusCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs index 152e220..2360617 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateBusCommandHandler : } var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateBusCommandHandler : "Bus with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.BusRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs index 4224d6f..74c6b26 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateBusCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs index 26ba345..b1fc747 100644 --- a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -24,7 +24,8 @@ public class GetBusQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.BusRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs index cf97c50..343e576 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs @@ -13,11 +13,9 @@ public record GetBusesPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } + public Guid? CompanyGuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityGreaterOrEqualThan { get; set; } - - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs index 920ea9e..28fe46f 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetBusesPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && - (request.CapacityGreaterOrEqualThan != null - ? e.Capacity >= request.CapacityGreaterOrEqualThan + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid : true) && - (request.CapacityLessOrEqualThan != null - ? e.Capacity <= request.CapacityLessOrEqualThan + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs index 4be8485..0820527 100644 --- a/src/Application/Buses/ViewModels/AddBusViewModel.cs +++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddBusViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs index a05dd27..725d0e9 100644 --- a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs +++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs @@ -2,14 +2,9 @@ namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; public sealed class GetBusesPageFilterViewModel { - public string? Number { get; set; } + public Guid? CompanyUuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - // TODO: Consider adding strict equals rule although it is not - // necessarily needed to filter with exact capacity - - public short? CapacityGreaterOrEqualThan { get; set; } - - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs index 6ee2c90..7ce4cde 100644 --- a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs +++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateBusViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs new file mode 100644 index 0000000..aa2dadb --- /dev/null +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Common.FluentValidation; + +public static class CustomValidators +{ + // According to RFC 5321. + public static IRuleBuilderOptions IsEmail( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$"); + } + + // According to ITU-T E.164, no spaces. + public static IRuleBuilderOptions IsPhoneNumber( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^\+[0-9]{7,15}$"); + } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs new file mode 100644 index 0000000..b65663a --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface CompanyRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 5cd0770..6db81b1 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -26,6 +26,8 @@ public interface UnitOfWork : IDisposable RouteAddressRepository RouteAddressRepository { get; } + CompanyRepository CompanyRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs new file mode 100644 index 0000000..5965a59 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public record AddCompanyCommand : IRequest +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..7d77ac2 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public class AddCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs new file mode 100644 index 0000000..07b4270 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public class AddCompanyCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCompanyCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Name == request.Name, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Company with given name already exists."); + } + + entity = new Company() + { + Name = request.Name, + LegalAddress = request.LegalAddress, + ContactEmail = request.ContactEmail, + ContactPhoneNumber = request.ContactPhoneNumber + }; + + entity = await _unitOfWork.CompanyRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs new file mode 100644 index 0000000..209a093 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs @@ -0,0 +1,58 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public class AddCompanyCommandValidator : AbstractValidator +{ + public AddCompanyCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.LegalAddress) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactPhoneNumber) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsPhoneNumber() + .WithMessage(localizer["FluentValidation.IsPhoneNumber"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs new file mode 100644 index 0000000..a9c6a71 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public record DeleteCompanyCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..bfd16e8 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs new file mode 100644 index 0000000..9ceae16 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCompanyCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CompanyRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs new file mode 100644 index 0000000..f926330 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandValidator : AbstractValidator +{ + public DeleteCompanyCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs new file mode 100644 index 0000000..d423b2d --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public record UpdateCompanyCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..5f93add --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs new file mode 100644 index 0000000..78e408e --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCompanyCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + entity.Name = request.Name; + entity.LegalAddress = request.LegalAddress; + entity.ContactEmail = request.ContactEmail; + entity.ContactPhoneNumber = request.ContactPhoneNumber; + + entity = await _unitOfWork.CompanyRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs new file mode 100644 index 0000000..a5bb800 --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs @@ -0,0 +1,62 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandValidator : AbstractValidator +{ + public UpdateCompanyCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.LegalAddress) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactPhoneNumber) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsPhoneNumber() + .WithMessage(localizer["FluentValidation.IsPhoneNumber"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Companies/CompanyDto.cs b/src/Application/Companies/CompanyDto.cs new file mode 100644 index 0000000..3bdc207 --- /dev/null +++ b/src/Application/Companies/CompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Companies; + +public sealed class CompanyDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs new file mode 100644 index 0000000..01a8c8f --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public record GetCompaniesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs new file mode 100644 index 0000000..979329c --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCompaniesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCompaniesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs new file mode 100644 index 0000000..49724c2 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs @@ -0,0 +1,49 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCompaniesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCompaniesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CompanyRepository.GetPageAsync( + e => + e.Name.ToLower().Contains(request.Search.ToLower()) || + e.LegalAddress.ToLower().Contains(request.Search.ToLower()) || + e.ContactEmail.ToLower().Contains(request.Search.ToLower()) || + e.ContactPhoneNumber.ToLower().Contains(request.Search.ToLower()), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs new file mode 100644 index 0000000..9f9143c --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryValidator : AbstractValidator +{ + public GetCompaniesPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs new file mode 100644 index 0000000..95de5b3 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public record GetCompanyQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs new file mode 100644 index 0000000..ce96fe7 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public class GetCompanyQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCompanyQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCompanyQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs new file mode 100644 index 0000000..61cc0eb --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public class GetCompanyQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCompanyQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCompanyQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs new file mode 100644 index 0000000..99db2fd --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public class GetCompanyQueryValidator : AbstractValidator +{ + public GetCompanyQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Companies/ViewModels/AddCompanyViewModel.cs b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs new file mode 100644 index 0000000..2927de8 --- /dev/null +++ b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Companies.ViewModels; + +public sealed class AddCompanyViewModel +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs b/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs new file mode 100644 index 0000000..535ba6a --- /dev/null +++ b/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Companies.ViewModels; + +public sealed class UpdateCompanyViewModel +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index ff87349..8d104df 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -4,7 +4,9 @@ "NotEmpty": "Must not be empty.", "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", - "MustBeInEnum": "Must be one of the following: {0}." + "MustBeInEnum": "Must be one of the following: {0}.", + "IsEmail": "Must be a valid email address according to RFC 5321.", + "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters." }, "Validation": { "DistinctOrder": "Must have distinct order values.", diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs index a5e81d1..6c680e5 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs @@ -9,4 +9,6 @@ public record AddTrainCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs index 8edd60c..4644d93 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -33,11 +33,22 @@ public class AddTrainCommandHandler : "Train with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity = new Train() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.TrainRepository.AddOneAsync( diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs index ec530dd..092bbc9 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -33,5 +33,9 @@ public class AddTrainCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs index 4aa128f..4e40e13 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs @@ -11,4 +11,6 @@ public record UpdateTrainCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs index 70ea0ce..e7be03f 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateTrainCommandHandler : } var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateTrainCommandHandler : "Train with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.TrainRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs index b39eac1..b23fb12 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateTrainCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs index b0c89fd..afe796b 100644 --- a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -24,7 +24,8 @@ public class GetTrainQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.TrainRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs index ba41889..1160f90 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs @@ -13,9 +13,7 @@ public record GetTrainsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } - - public string? Model { get; set; } + public Guid? CompanyGuid { get; set; } public short? CapacityGreaterOrEqualThan { get; set; } diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs index 2690cfa..49a0c7d 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetTrainsPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && (request.CapacityGreaterOrEqualThan != null ? e.Capacity >= request.CapacityGreaterOrEqualThan : true) && (request.CapacityLessOrEqualThan != null ? e.Capacity <= request.CapacityLessOrEqualThan : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Trains/TrainDto.cs b/src/Application/Trains/TrainDto.cs index 2e02c73..2b1cf05 100644 --- a/src/Application/Trains/TrainDto.cs +++ b/src/Application/Trains/TrainDto.cs @@ -13,11 +13,16 @@ public sealed class TrainDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Trains/ViewModels/AddTrainViewModel.cs b/src/Application/Trains/ViewModels/AddTrainViewModel.cs index 998097e..59a75c8 100644 --- a/src/Application/Trains/ViewModels/AddTrainViewModel.cs +++ b/src/Application/Trains/ViewModels/AddTrainViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddTrainViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs index ef0026b..fed89ef 100644 --- a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs +++ b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs @@ -2,14 +2,12 @@ namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; public sealed class GetTrainsPageFilterViewModel { - public string? Number { get; set; } - - public string? Model { get; set; } - // TODO: Consider adding strict equals rule although it is not // necessarily needed to filter with exact capacity - public short? CapacityGreaterOrEqualThan { get; set; } + public Guid? CompanyUuid { get; set; } - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs index 3b7682e..3458e86 100644 --- a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs +++ b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateTrainViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs new file mode 100644 index 0000000..a5322fc --- /dev/null +++ b/src/Domain/Entities/Company.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Company : EntityBase +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + + public ICollection Vehicles { get; set; } +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs index 42196e8..f0fb3c9 100644 --- a/src/Domain/Entities/Vehicle.cs +++ b/src/Domain/Entities/Vehicle.cs @@ -7,5 +7,10 @@ public abstract class Vehicle : EntityBase public VehicleType VehicleType { get; set; } + public long CompanyId { get; set; } + + public Company Company { get; set; } + + public ICollection Enrollments { get; set; } } diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs index 2c040e8..620b581 100644 --- a/src/HttpApi/Controllers/AircraftsController.cs +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; -using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Aircrafts; using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; @@ -51,7 +50,8 @@ public class AircraftsController : ControllerBase { Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken)); } @@ -87,10 +87,11 @@ public class AircraftsController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, - CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } @@ -158,7 +159,8 @@ public class AircraftsController : ControllerBase Guid = uuid, Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken); } diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs index c5d55a5..bd68d37 100644 --- a/src/HttpApi/Controllers/BusesController.cs +++ b/src/HttpApi/Controllers/BusesController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; -using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Buses; using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; @@ -51,7 +50,8 @@ public class BusesController : ControllerBase { Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken)); } @@ -87,10 +87,11 @@ public class BusesController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, - CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } @@ -158,7 +159,8 @@ public class BusesController : ControllerBase Guid = uuid, Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken); } diff --git a/src/HttpApi/Controllers/CompaniesController.cs b/src/HttpApi/Controllers/CompaniesController.cs new file mode 100644 index 0000000..a8ca669 --- /dev/null +++ b/src/HttpApi/Controllers/CompaniesController.cs @@ -0,0 +1,190 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Companies; +using cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; +using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; +using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; +using cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; +using cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; +using cuqmbr.TravelGuide.Application.Companies.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("companies")] +public class CompaniesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a company")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddCompanyViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddCompanyCommand() + { + Name = viewModel.Name, + LegalAddress = viewModel.LegalAddress, + ContactEmail = viewModel.ContactEmail, + ContactPhoneNumber = viewModel.ContactPhoneNumber, + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all companies")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCompaniesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a company by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCompanyQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a company")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateCompanyViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateCompanyCommand() + { + Guid = uuid, + Name = viewModel.Name, + LegalAddress = viewModel.LegalAddress, + ContactEmail = viewModel.ContactEmail, + ContactPhoneNumber = viewModel.ContactPhoneNumber, + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a company")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteCompanyCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs index fac873a..b1bb992 100644 --- a/src/HttpApi/Controllers/TrainsController.cs +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -88,9 +88,9 @@ public class TrainsController : ControllerBase Search = searchQuery.Search, Sort = sortQuery.Sort, CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, + filterQuery.CapacityGreaterThanOrEqualTo, CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index cde7f75..0773c64 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -26,6 +26,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork new InMemoryVehicleEnrollmentRepository(_dbContext); RouteAddressRepository = new InMemoryRouteAddressRepository(_dbContext); + CompanyRepository = new InMemoryCompanyRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -50,6 +51,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public RouteAddressRepository RouteAddressRepository { get; init; } + public CompanyRepository CompanyRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs new file mode 100644 index 0000000..66b2516 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryCompanyRepository : + InMemoryBaseRepository, CompanyRepository +{ + public InMemoryCompanyRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs new file mode 100644 index 0000000..b01666c --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class CompanyConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("companies"); + + base.Configure(builder); + + + builder + .Property(c => c.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(c => c.LegalAddress) + .HasColumnName("legal_address") + .HasColumnType("varchar(256)") + .IsRequired(true); + + builder + .Property(c => c.ContactEmail) + .HasColumnName("contact_email") + .HasColumnType("varchar(256)") + .IsRequired(true); + + builder + .Property(c => c.ContactPhoneNumber) + .HasColumnName("contact_phone_number") + .HasColumnType("varchar(64)") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs index e95accf..d2c0396 100644 --- a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs @@ -37,5 +37,29 @@ public class VehicleConfiguration : BaseConfiguration .HasValue(VehicleType.Train); base.Configure(builder); + + + builder + .Property(v => v.CompanyId) + .HasColumnName("company_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(v => v.Company) + .WithMany(c => c.Vehicles) + .HasForeignKey(v => v.CompanyId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(v => v.CompanyId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs new file mode 100644 index 0000000..a91ef1f --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs @@ -0,0 +1,706 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250515101417_Add_Companies")] + partial class Add_Companies + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs new file mode 100644 index 0000000..501e528 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Companies : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments"); + + migrationBuilder.CreateSequence( + name: "companies_id_sequence", + schema: "application"); + + migrationBuilder.AddColumn( + name: "company_id", + schema: "application", + table: "vehicles", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateTable( + name: "companies", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.companies_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + legal_address = table.Column(type: "varchar(256)", nullable: false), + contact_email = table.Column(type: "varchar(256)", nullable: false), + contact_phone_number = table.Column(type: "varchar(64)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_companies", x => x.id); + table.UniqueConstraint("altk_companies_uuid", x => x.uuid); + }); + + migrationBuilder.CreateIndex( + name: "ix_vehicles_company_id", + schema: "application", + table: "vehicles", + column: "company_id"); + + migrationBuilder.AddCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments", + sql: "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + + migrationBuilder.AddForeignKey( + name: "fk_vehicles_company_id", + schema: "application", + table: "vehicles", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_vehicles_company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropTable( + name: "companies", + schema: "application"); + + migrationBuilder.DropIndex( + name: "ix_vehicles_company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments"); + + migrationBuilder.DropColumn( + name: "company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropSequence( + name: "companies_id_sequence", + schema: "application"); + + migrationBuilder.AddCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments", + sql: "currency IN ('USD', 'EUR', 'UAH')"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index b480530..2d7ebeb 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -27,6 +27,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("cities_id_sequence"); + modelBuilder.HasSequence("companies_id_sequence"); + modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); @@ -125,6 +127,49 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("cities", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Property("Id") @@ -327,6 +372,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + b.Property("Guid") .HasColumnType("uuid") .HasColumnName("uuid"); @@ -342,6 +391,9 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_vehicles_uuid"); + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + b.ToTable("vehicles", "application", t => { t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); @@ -397,7 +449,7 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("vehicle_enrollments", "application", t => { - t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); }); }); @@ -566,6 +618,18 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("VehicleEnrollment"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") @@ -597,6 +661,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Addresses"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Vehicles"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Navigation("Regions"); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 0e588c2..11eab91 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -26,6 +26,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork new PostgreSqlVehicleEnrollmentRepository(_dbContext); RouteAddressRepository = new PostgreSqlRouteAddressRepository(_dbContext); + CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -50,6 +51,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public RouteAddressRepository RouteAddressRepository { get; init; } + public CompanyRepository CompanyRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs new file mode 100644 index 0000000..ed857cd --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlCompanyRepository : + PostgreSqlBaseRepository, CompanyRepository +{ + public PostgreSqlCompanyRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} From 5982fa7285f09b6e77ee568cd93fd1eedbc7f000 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 16 May 2025 15:22:44 +0300 Subject: [PATCH 15/35] add employee management --- .../Repositories/EmployeeRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../AddEmployee/AddEmployeeCommand.cs | 23 + .../AddEmployeeCommandAuthorizer.cs | 31 + .../AddEmployee/AddEmployeeCommandHandler.cs | 80 ++ .../AddEmployeeCommandValidator.cs | 83 ++ .../DeleteEmployee/DeleteEmployeeCommand.cs | 8 + .../DeleteEmployeeCommandAuthorizer.cs | 31 + .../DeleteEmployeeCommandHandler.cs | 37 + .../DeleteEmployeeCommandValidator.cs | 14 + .../UpdateEmployee/UpdateEmployeeCommand.cs | 25 + .../UpdateEmployeeCommandAuthorizer.cs | 31 + .../UpdateEmployeeCommandHandler.cs | 108 +++ .../UpdateEmployeeCommandValidator.cs | 87 ++ .../Employees/EmployeeDocumentDto.cs | 19 + src/Application/Employees/EmployeeDto.cs | 38 + .../Employees/Models/EmployeeDocumentModel.cs | 10 + .../Queries/GetEmployee/GetEmployeeQuery.cs | 8 + .../GetEmployee/GetEmployeeQueryAuthorizer.cs | 31 + .../GetEmployee/GetEmployeeQueryHandler.cs | 48 + .../GetEmployee/GetEmployeeQueryValidator.cs | 14 + .../GetEmployeesPage/GetEmployeesPageQuery.cs | 24 + .../GetEmployeesPageQueryAuthorizer.cs | 31 + .../GetEmployeesPageQueryHandler.cs | 78 ++ .../GetEmployeesPageQueryValidator.cs | 43 + .../ViewModels/AddEmployeeViewModel.cs | 19 + .../ViewModels/EmployeeDocumentViewModel.cs | 8 + .../GetEmployeesPageFilterViewModel.cs | 12 + .../ViewModels/UpdateEmployeeViewModel.cs | 19 + .../Resources/Localization/en-US.json | 4 +- src/Domain/Entities/Company.cs | 2 + src/Domain/Entities/Employee.cs | 23 + src/Domain/Entities/EmployeeDocument.cs | 15 + src/Domain/Entities/VehicleEnrollment.cs | 3 + src/Domain/Enums/DocumentType.cs | 16 + src/Domain/Enums/Sex.cs | 22 + .../Controllers/EmployeesController.cs | 212 +++++ src/Persistence/InMemory/InMemoryDbContext.cs | 6 - .../InMemory/InMemoryUnitOfWork.cs | 3 + .../InMemoryEmployeeRepository.cs | 11 + .../Configurations/EmployeeConfiguration.cs | 81 ++ .../EmployeeDocumentConfiguration.cs | 63 ++ ..._Employee_and_EmployeeDocument.Designer.cs | 841 ++++++++++++++++++ ...64353_Add_Employee_and_EmployeeDocument.cs | 108 +++ .../PostgreSqlDbContextModelSnapshot.cs | 135 +++ .../PostgreSql/PostgreSqlDbContext.cs | 10 + .../PostgreSql/PostgreSqlUnitOfWork.cs | 3 + .../PostgreSqlEmployeeRepository.cs | 11 + .../TypeConverters/DocumentTypeConverter.cs | 13 + .../TypeConverters/SexConverter.cs | 13 + 50 files changed, 2555 insertions(+), 8 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/EmployeeDocumentDto.cs create mode 100644 src/Application/Employees/EmployeeDto.cs create mode 100644 src/Application/Employees/Models/EmployeeDocumentModel.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs create mode 100644 src/Application/Employees/ViewModels/AddEmployeeViewModel.cs create mode 100644 src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs create mode 100644 src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs create mode 100644 src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs create mode 100644 src/Domain/Entities/Employee.cs create mode 100644 src/Domain/Entities/EmployeeDocument.cs create mode 100644 src/Domain/Enums/DocumentType.cs create mode 100644 src/Domain/Enums/Sex.cs create mode 100644 src/HttpApi/Controllers/EmployeesController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs create mode 100644 src/Persistence/TypeConverters/DocumentTypeConverter.cs create mode 100644 src/Persistence/TypeConverters/SexConverter.cs diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs new file mode 100644 index 0000000..012cf93 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface EmployeeRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 6db81b1..7c0b461 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -28,6 +28,8 @@ public interface UnitOfWork : IDisposable CompanyRepository CompanyRepository { get; } + EmployeeRepository EmployeeRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs new file mode 100644 index 0000000..4fe21d7 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public record AddEmployeeCommand : IRequest +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyGuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..8e2f0d1 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddEmployeeCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs new file mode 100644 index 0000000..1d528b6 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -0,0 +1,80 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public AddEmployeeCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + AddEmployeeCommand request, + CancellationToken cancellationToken) + { + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => + e.FirstName == request.FirstName && + e.LastName == request.LastName && + e.Patronymic == request.Patronymic && + e.Sex == request.Sex && + e.BirthDate == request.BirthDate && + e.CompanyId == parentEntity.Id, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException(); + } + + entity = new Employee() + { + FirstName = request.FirstName, + LastName = request.LastName, + Patronymic = request.Patronymic, + Sex = request.Sex, + BirthDate = request.BirthDate, + Documents = request.Documents.Select( + d => new EmployeeDocument() + { + DocumentType = d.DocumentType, + Information = d.Information + }) + .ToArray(), + Company = parentEntity + }; + + entity = await _unitOfWork.EmployeeRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs new file mode 100644 index 0000000..149bc95 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs @@ -0,0 +1,83 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandValidator : AbstractValidator +{ + public AddEmployeeCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(e => e.FirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.LastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Patronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Sex) + .Must((e, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(e => e.BirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleForEach(e => e.Documents).ChildRules(d => + { + d.RuleFor(d => d.DocumentType) + .Must(dt => DocumentType.Enumerations.ContainsValue(dt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + DocumentType.Enumerations.Values.Select(e => e.Name)))); + + d.RuleFor(d => d.Information) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + }); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs new file mode 100644 index 0000000..4a1d1be --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public record DeleteEmployeeCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..e2f0478 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteEmployeeCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs new file mode 100644 index 0000000..2e7c8c3 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs @@ -0,0 +1,37 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteEmployeeCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteEmployeeCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for Vehicles that using this employee in Enrollments + // Delete if there are no such Vehicles + + await _unitOfWork.EmployeeRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs new file mode 100644 index 0000000..9509930 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandValidator : AbstractValidator +{ + public DeleteEmployeeCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs new file mode 100644 index 0000000..883a6a4 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs @@ -0,0 +1,25 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public record UpdateEmployeeCommand : IRequest +{ + public Guid Guid { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyGuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..e3ff38e --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateEmployeeCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs new file mode 100644 index 0000000..80d3d26 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs @@ -0,0 +1,108 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + public IStringLocalizer _localizer { get; set; } + + public UpdateEmployeeCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateEmployeeCommand request, + CancellationToken cancellationToken) + { + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + + + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => + e.FirstName == request.FirstName && + e.LastName == request.LastName && + e.Patronymic == request.Patronymic && + e.Sex == request.Sex && + e.BirthDate == request.BirthDate && + e.CompanyId == parentEntity.Id && + e.Guid != request.Guid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException(); + } + + + entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Documents, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + + entity.Guid = request.Guid; + entity.FirstName = request.FirstName; + entity.LastName = request.LastName; + entity.Patronymic = request.Patronymic; + entity.Sex = request.Sex; + entity.BirthDate = request.BirthDate; + entity.CompanyId = parentEntity.Id; + + entity.Company = parentEntity; + + + var requestEmployeeDocuments = request.Documents.Select( + d => new EmployeeDocument() + { + DocumentType = d.DocumentType, + Information = d.Information + }); + + var commonEmployeeDocuments = entity.Documents.IntersectBy( + requestEmployeeDocuments.Select( + ed => (ed.DocumentType, ed.Information)), + ed => (ed.DocumentType, ed.Information)); + + var newEmployeeDocuments = requestEmployeeDocuments.ExceptBy( + entity.Documents.Select(ed => (ed.DocumentType, ed.Information)), + ed => (ed.DocumentType, ed.Information)); + + var combinedEmployeeDocuments = commonEmployeeDocuments.UnionBy( + newEmployeeDocuments, ed => (ed.DocumentType, ed.Information)); + + entity.Documents = combinedEmployeeDocuments.ToList(); + + + entity = await _unitOfWork.EmployeeRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs new file mode 100644 index 0000000..e6cf45e --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs @@ -0,0 +1,87 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandValidator : AbstractValidator +{ + public UpdateEmployeeCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(e => e.FirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.LastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Patronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Sex) + .Must((e, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(e => e.BirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleForEach(e => e.Documents).ChildRules(d => + { + d.RuleFor(d => d.DocumentType) + .Must(dt => DocumentType.Enumerations.ContainsValue(dt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + DocumentType.Enumerations.Values.Select(e => e.Name)))); + + d.RuleFor(d => d.Information) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + }); + } +} diff --git a/src/Application/Employees/EmployeeDocumentDto.cs b/src/Application/Employees/EmployeeDocumentDto.cs new file mode 100644 index 0000000..8865f14 --- /dev/null +++ b/src/Application/Employees/EmployeeDocumentDto.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeDocumentDto : IMapFrom +{ + public string DocumentType { get; set; } + + public string Information { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.DocumentType, + opt => opt.MapFrom(s => s.DocumentType.Name)); + } +} diff --git a/src/Application/Employees/EmployeeDto.cs b/src/Application/Employees/EmployeeDto.cs new file mode 100644 index 0000000..fec500f --- /dev/null +++ b/src/Application/Employees/EmployeeDto.cs @@ -0,0 +1,38 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Sex, + opt => opt.MapFrom(s => s.Sex.Name)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); + } +} diff --git a/src/Application/Employees/Models/EmployeeDocumentModel.cs b/src/Application/Employees/Models/EmployeeDocumentModel.cs new file mode 100644 index 0000000..2992dfa --- /dev/null +++ b/src/Application/Employees/Models/EmployeeDocumentModel.cs @@ -0,0 +1,10 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Employees.Models; + +public sealed class EmployeeDocumentModel +{ + public DocumentType DocumentType { get; set; } + + public string Information { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs new file mode 100644 index 0000000..67a4024 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public record GetEmployeeQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs new file mode 100644 index 0000000..6011990 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetEmployeeQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetEmployeeQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs new file mode 100644 index 0000000..2e3edcb --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs @@ -0,0 +1,48 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeeQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetEmployeeQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Documents, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + + // Hydrate employees with companies + + var company = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Id == entity.CompanyId, cancellationToken); + + entity.Company = company; + + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs new file mode 100644 index 0000000..da6a0ef --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryValidator : AbstractValidator +{ + public GetEmployeeQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs new file mode 100644 index 0000000..209fcc2 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public record GetEmployeesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Sex? Sex { get; set; } + + public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + + public Guid? CompanyGuid { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs new file mode 100644 index 0000000..775eeb3 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetEmployeesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetEmployeesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs new file mode 100644 index 0000000..21a5a81 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -0,0 +1,78 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetEmployeesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.EmployeeRepository.GetPageAsync( + e => + (e.FirstName.ToLower().Contains(request.Search.ToLower()) || + e.LastName.ToLower().Contains(request.Search.ToLower()) || + e.Patronymic.ToLower().Contains(request.Search.ToLower()) || + e.Documents + .Select(d => d.Information.ToLower()) + .Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && + (request.Sex != null + ? e.Sex == request.Sex + : true) && + (request.BirthDateLessThanOrEqualTo != null + ? e.BirthDate <= request.BirthDateLessThanOrEqualTo + : true) && + (request.BirthDateGreaterThanOrEqualTo != null + ? e.BirthDate >= request.BirthDateGreaterThanOrEqualTo + : true), + e => e.Documents, + request.PageNumber, request.PageSize, + cancellationToken); + + + // Hydrate employees with companies + + var companies = await _unitOfWork.CompanyRepository.GetPageAsync( + e => paginatedList.Items.Select(e => e.CompanyId).Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var employee in paginatedList.Items) + { + employee.Company = + companies.Items.First(c => c.Id == employee.CompanyId); + } + + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs new file mode 100644 index 0000000..c85edf2 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryValidator : AbstractValidator +{ + public GetEmployeesPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs new file mode 100644 index 0000000..97ba828 --- /dev/null +++ b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs @@ -0,0 +1,19 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class AddEmployeeViewModel +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs new file mode 100644 index 0000000..bb1155e --- /dev/null +++ b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class EmployeeDocumentViewModel +{ + public string DocumentType { get; set; } + + public string Information { get; set; } +} diff --git a/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs new file mode 100644 index 0000000..8a6e59d --- /dev/null +++ b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class GetEmployeesPageFilterViewModel +{ + public string? Sex { get; set; } + + public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + + public Guid? CompanyUuid { get; set; } +} diff --git a/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs new file mode 100644 index 0000000..0d2f4ed --- /dev/null +++ b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs @@ -0,0 +1,19 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class UpdateEmployeeViewModel +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 8d104df..65c8189 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -2,8 +2,8 @@ "FluentValidation": { "MaximumLength": "Must less than {0:G} characters.", "NotEmpty": "Must not be empty.", - "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", - "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", + "GreaterThanOrEqualTo": "Must be greater than or equal to {0}.", + "LessThanOrEqualTo": "Must be less than or equal to {0}.", "MustBeInEnum": "Must be one of the following: {0}.", "IsEmail": "Must be a valid email address according to RFC 5321.", "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters." diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs index a5322fc..cab7f8e 100644 --- a/src/Domain/Entities/Company.cs +++ b/src/Domain/Entities/Company.cs @@ -11,5 +11,7 @@ public sealed class Company : EntityBase public string ContactPhoneNumber { get; set; } + public ICollection Employees { get; set; } + public ICollection Vehicles { get; set; } } diff --git a/src/Domain/Entities/Employee.cs b/src/Domain/Entities/Employee.cs new file mode 100644 index 0000000..ec518ce --- /dev/null +++ b/src/Domain/Entities/Employee.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Employee : EntityBase +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public long CompanyId { get; set; } + + public Company Company { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Domain/Entities/EmployeeDocument.cs b/src/Domain/Entities/EmployeeDocument.cs new file mode 100644 index 0000000..861c33c --- /dev/null +++ b/src/Domain/Entities/EmployeeDocument.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class EmployeeDocument : EntityBase +{ + public DocumentType DocumentType { get; set; } + + public string Information { get; set; } + + + public long EmployeeId { get; set; } + + public Employee Employee { get; set; } +} diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs index ac92112..31ae842 100644 --- a/src/Domain/Entities/VehicleEnrollment.cs +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -20,4 +20,7 @@ public class VehicleEnrollment : EntityBase public ICollection RouteAddressDetails { get; set; } + + + public ICollection Tickets { get; set; } } diff --git a/src/Domain/Enums/DocumentType.cs b/src/Domain/Enums/DocumentType.cs new file mode 100644 index 0000000..bde01d7 --- /dev/null +++ b/src/Domain/Enums/DocumentType.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class DocumentType : Enumeration +{ + public static readonly DocumentType Passport = new PassportDocumentType(); + + protected DocumentType(int value, string name) : base(value, name) { } + + private sealed class PassportDocumentType : DocumentType + { + public PassportDocumentType() : base(0, "passport") { } + } +} diff --git a/src/Domain/Enums/Sex.cs b/src/Domain/Enums/Sex.cs new file mode 100644 index 0000000..f1a8460 --- /dev/null +++ b/src/Domain/Enums/Sex.cs @@ -0,0 +1,22 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class Sex : Enumeration +{ + public static readonly Sex Male = new MaleSex(); + public static readonly Sex Female = new FemaleSex(); + + protected Sex(int value, string name) : base(value, name) { } + + private sealed class MaleSex : Sex + { + public MaleSex() : base(Int32.MaxValue, "male") { } + } + + private sealed class FemaleSex : Sex + { + public FemaleSex() : base(Int32.MinValue, "female") { } + } +} diff --git a/src/HttpApi/Controllers/EmployeesController.cs b/src/HttpApi/Controllers/EmployeesController.cs new file mode 100644 index 0000000..4df1d5b --- /dev/null +++ b/src/HttpApi/Controllers/EmployeesController.cs @@ -0,0 +1,212 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Employees; +using cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; +using cuqmbr.TravelGuide.Application.Employees.ViewModels; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("employees")] +public class EmployeesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add an employee")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddEmployeeCommand() + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all employees")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetEmployeesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetEmployeesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + Sex = Sex.FromName(filterQuery.Sex), + BirthDateLessThanOrEqualTo = + filterQuery.BirthDateLessThanOrEqualTo, + BirthDateGreaterThanOrEqualTo = + filterQuery.BirthDateGreaterThanOrEqualTo, + CompanyGuid = filterQuery.CompanyUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get an employee by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetEmployeeQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update an employee")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateEmployeeCommand() + { + Guid = uuid, + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete an employee")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteEmployeeCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 9b2f919..e27b3ba 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,5 +1,4 @@ using cuqmbr.TravelGuide.Domain.Enums; -using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; using cuqmbr.TravelGuide.Persistence.TypeConverters; @@ -10,11 +9,6 @@ public class InMemoryDbContext : DbContext public InMemoryDbContext(DbContextOptions options) : base(options) { } - public DbSet Countries { get => Set(); } - public DbSet Regions { get => Set(); } - public DbSet Cities { get => Set(); } - public DbSet
Addresses { get => Set
(); } - protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 0773c64..f7f1d15 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -27,6 +27,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork RouteAddressRepository = new InMemoryRouteAddressRepository(_dbContext); CompanyRepository = new InMemoryCompanyRepository(_dbContext); + EmployeeRepository = new InMemoryEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -53,6 +54,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public CompanyRepository CompanyRepository { get; init; } + public EmployeeRepository EmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs new file mode 100644 index 0000000..40e9b63 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryEmployeeRepository : + InMemoryBaseRepository, EmployeeRepository +{ + public InMemoryEmployeeRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs new file mode 100644 index 0000000..fd4ccb5 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs @@ -0,0 +1,81 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class EmployeeConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(e => e.Sex) + .HasColumnName("sex") + .IsRequired(true); + + builder + .ToTable( + "employees", + e => e.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()}", + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(e => e.FirstName) + .HasColumnName("first_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.LastName) + .HasColumnName("last_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.Patronymic) + .HasColumnName("patronymic") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.BirthDate) + .HasColumnName("birth_date") + .HasColumnType("date") + .IsRequired(true); + + + builder + .Property(e => e.CompanyId) + .HasColumnName("company_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(e => e.Company) + .WithMany(c => c.Employees) + .HasForeignKey(e => e.CompanyId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(e => e.CompanyId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs new file mode 100644 index 0000000..1878cc6 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs @@ -0,0 +1,63 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class EmployeeDocumentConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(ed => ed.DocumentType) + .HasColumnName("document_type") + .IsRequired(true); + + builder + .ToTable( + "employee_documents", + ed => ed.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()}", + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", DocumentType.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(ed => ed.Information) + .HasColumnName("information") + .HasColumnType("varchar(256)") + .IsRequired(true); + + + builder + .Property(ed => ed.EmployeeId) + .HasColumnName("employee_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ed => ed.Employee) + .WithMany(ed => ed.Documents) + .HasForeignKey(ed => ed.EmployeeId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ed => ed.EmployeeId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs new file mode 100644 index 0000000..04dca68 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs @@ -0,0 +1,841 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250515164353_Add_Employee_and_EmployeeDocument")] + partial class Add_Employee_and_EmployeeDocument + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs new file mode 100644 index 0000000..1f8fc90 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Employee_and_EmployeeDocument : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "employees_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "employees", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employees_id_sequence')"), + first_name = table.Column(type: "varchar(32)", nullable: false), + last_name = table.Column(type: "varchar(32)", nullable: false), + patronymic = table.Column(type: "varchar(32)", nullable: false), + sex = table.Column(type: "varchar(32)", nullable: false), + birth_date = table.Column(type: "date", nullable: false), + company_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employees", x => x.id); + table.UniqueConstraint("altk_employees_uuid", x => x.uuid); + table.CheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + table.ForeignKey( + name: "fk_employees_company_id", + column: x => x.company_id, + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "employee_documents", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employee_documents_id_sequence')"), + document_type = table.Column(type: "varchar(64)", nullable: false), + information = table.Column(type: "varchar(256)", nullable: false), + employee_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employee_documents", x => x.id); + table.UniqueConstraint("altk_employee_documents_uuid", x => x.uuid); + table.CheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + table.ForeignKey( + name: "fk_employee_documents_employee_id", + column: x => x.employee_id, + principalSchema: "application", + principalTable: "employees", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_employee_documents_employee_id", + schema: "application", + table: "employee_documents", + column: "employee_id"); + + migrationBuilder.CreateIndex( + name: "ix_employees_company_id", + schema: "application", + table: "employees", + column: "company_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "employee_documents", + schema: "application"); + + migrationBuilder.DropTable( + name: "employees", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employees_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 2d7ebeb..c2eb06f 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -31,6 +31,10 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("countries_id_sequence"); + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + modelBuilder.HasSequence("regions_id_sequence"); modelBuilder.HasSequence("route_address_details_id_sequence"); @@ -198,6 +202,106 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("countries", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Property("Id") @@ -564,6 +668,30 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Region"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") @@ -663,6 +791,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => { + b.Navigation("Employees"); + b.Navigation("Vehicles"); }); @@ -671,6 +801,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Regions"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Navigation("Cities"); diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index c9e667e..11e5d63 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -44,6 +44,16 @@ public class PostgreSqlDbContext : DbContext .HaveColumnType("varchar(8)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 11eab91..46ff3a9 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -27,6 +27,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork RouteAddressRepository = new PostgreSqlRouteAddressRepository(_dbContext); CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); + EmployeeRepository = new PostgreSqlEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -53,6 +54,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public CompanyRepository CompanyRepository { get; init; } + public EmployeeRepository EmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs new file mode 100644 index 0000000..03319c3 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlEmployeeRepository : + PostgreSqlBaseRepository, EmployeeRepository +{ + public PostgreSqlEmployeeRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/TypeConverters/DocumentTypeConverter.cs b/src/Persistence/TypeConverters/DocumentTypeConverter.cs new file mode 100644 index 0000000..83e2632 --- /dev/null +++ b/src/Persistence/TypeConverters/DocumentTypeConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class DocumentTypeConverter : ValueConverter +{ + public DocumentTypeConverter() + : base( + v => v.Name, + v => DocumentType.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/SexConverter.cs b/src/Persistence/TypeConverters/SexConverter.cs new file mode 100644 index 0000000..f3d2fd5 --- /dev/null +++ b/src/Persistence/TypeConverters/SexConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class SexConverter : ValueConverter +{ + public SexConverter() + : base( + v => v.Name, + v => Sex.FromName(v)) + { } +} From 6830fea5633e960dc73010fcee41322924f8eae4 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 20 May 2025 20:39:09 +0300 Subject: [PATCH 16/35] add ticket group creation --- .../FluentValidation/CustomValidators.cs | 31 + .../RouteAddressDetailRepository.cs | 7 + .../Repositories/TicketGroupRepository.cs | 6 + .../Repositories/TicketRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 6 + .../Resources/Localization/en-US.json | 3 +- .../AddTicketGroup/AddTicketGroupCommand.cs | 25 + .../AddTicketGroupCommandAuthorizer.cs | 31 + .../AddTicketGroupCommandHandler.cs | 477 ++++++++ .../AddTicketGroupCommandValidator.cs | 107 ++ .../TicketGroups/Models/TicketModel.cs | 13 + .../TicketGroups/TicketAddressDto.cs | 53 + src/Application/TicketGroups/TicketDto.cs | 50 + .../TicketGroups/TicketGroupDto.cs | 45 + .../ViewModels/AddTicketGroupViewModel.cs | 21 + .../GetTicketGroupsPageFilterViewModel.cs | 12 + .../ViewModels/TicketViewModel.cs | 13 + .../ViewModels/UpdateTicketGroupViewModel.cs | 21 + .../AddVehicleEnrollmentCommandHandler.cs | 18 +- .../UpdateVehicleEnrollmentCommandHandler.cs | 18 +- src/Domain/Entities/Ticket.cs | 30 + src/Domain/Entities/TicketGroup.cs | 25 + .../Controllers/TicketGroupsController.cs | 221 ++++ src/Persistence/InMemory/InMemoryDbContext.cs | 28 +- .../InMemory/InMemoryUnitOfWork.cs | 10 + .../InMemoryRouteAddressDetailRepository.cs | 11 + .../InMemoryTicketGroupRepository.cs | 11 + .../Repositories/InMemoryTicketRepository.cs | 11 + .../Configurations/TicketConfiguration.cs | 105 ++ .../TicketGroupConfiguration.cs | 75 ++ ...941_Add_Ticket_and_TicketGroup.Designer.cs | 1016 +++++++++++++++++ ...250519212941_Add_Ticket_and_TicketGroup.cs | 140 +++ .../PostgreSqlDbContextModelSnapshot.cs | 175 +++ .../PostgreSql/PostgreSqlUnitOfWork.cs | 10 + .../PostgreSqlRouteAddressDetailRepository.cs | 11 + .../PostgreSqlTicketGroupRepository.cs | 11 + .../PostgreSqlTicketRepository.cs | 11 + 37 files changed, 2843 insertions(+), 21 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs create mode 100644 src/Application/TicketGroups/Models/TicketModel.cs create mode 100644 src/Application/TicketGroups/TicketAddressDto.cs create mode 100644 src/Application/TicketGroups/TicketDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupDto.cs create mode 100644 src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/TicketViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs create mode 100644 src/Domain/Entities/Ticket.cs create mode 100644 src/Domain/Entities/TicketGroup.cs create mode 100644 src/HttpApi/Controllers/TicketGroupsController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs index aa2dadb..f7b1de1 100644 --- a/src/Application/Common/FluentValidation/CustomValidators.cs +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -21,4 +21,35 @@ public static class CustomValidators ruleBuilder .Matches(@"^\+[0-9]{7,15}$"); } + + public static IRuleBuilderOptions> + IsUnique( + this IRuleBuilder> ruleBuilder, + Func selector) + { + if (selector == null) + { + throw new ArgumentNullException( + nameof(selector), + "Cannot pass a null selector."); + } + + return + ruleBuilder + .Must(x => x.IsDistinct(selector)); + } + + public static bool IsDistinct( + this IEnumerable elements, Func selector) + { + var hashSet = new HashSet(); + foreach (var element in elements.Select(selector)) + { + if (!hashSet.Contains(element)) + hashSet.Add(element); + else + return false; + } + return true; + } } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs new file mode 100644 index 0000000..2bac9d4 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface RouteAddressDetailRepository : + BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs new file mode 100644 index 0000000..ead97c3 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TicketGroupRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs new file mode 100644 index 0000000..57b96aa --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TicketRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 7c0b461..de5c4ee 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -30,6 +30,12 @@ public interface UnitOfWork : IDisposable EmployeeRepository EmployeeRepository { get; } + TicketGroupRepository TicketGroupRepository { get; } + + TicketRepository TicketRepository { get; } + + RouteAddressDetailRepository RouteAddressDetailRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 65c8189..be9ac72 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -6,7 +6,8 @@ "LessThanOrEqualTo": "Must be less than or equal to {0}.", "MustBeInEnum": "Must be one of the following: {0}.", "IsEmail": "Must be a valid email address according to RFC 5321.", - "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters." + "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters.", + "IsUnique": "Elements of the collection must be unique." }, "Validation": { "DistinctOrder": "Must have distinct order values.", diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs new file mode 100644 index 0000000..554344b --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.TicketGroups.Models; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public record AddTicketGroupCommand : IRequest +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public bool Returned { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs new file mode 100644 index 0000000..b2c5deb --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public class AddTicketGroupCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddTicketGroupCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddTicketGroupCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs new file mode 100644 index 0000000..efe1455 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -0,0 +1,477 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation.Results; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +// TODO: Add descriptions and proper property names to validation errors + +public class AddTicketGroupCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + public AddTicketGroupCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + } + + public async Task Handle( + AddTicketGroupCommand request, + CancellationToken cancellationToken) + { + // Check whether provided vehicle enrollments are present in datastore. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + if (vehicleEnrollmentGuids.Count() > vehicleEnrollments.Count) + { + throw new NotFoundException(); + } + } + + + // Check whether provided arrival and departure address guids + // are used in provided vehicle enrollment and + // and are in the correct order. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + foreach (var t in request.Tickets) + { + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + if (departureRouteAddress.RouteId != ve.RouteId || + arrivalRouteAddress.RouteId != ve.RouteId) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + + if (departureRouteAddress.Order > arrivalRouteAddress.Order) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Check availability of free places. + { + // Get all tickets for vehicle enrollments requested in ticket group. + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var ticketGroupTickets = (await _unitOfWork.TicketRepository + .GetPageAsync( + e => + vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && + e.TicketGroup.Returned == false, + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Get all vehicle enrollments requested in ticket group + // together with vehicles. + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + e => e.Vehicle, + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + // Get all route addresses of vehicle enrollments + // requested in ticket group. + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeIds.Contains(e.RouteId), + 1, int.MaxValue, cancellationToken)) + .Items; + + // For each ticket in request. + foreach (var requestTicket in request.Tickets) + { + // Get vehicle enrollment of requested ticket. + var requestVehicleEnrollment = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid); + + // Get bought tickets of vehicle enrollment of requested ticket. + var tickets = ticketGroupTickets.Where(t => + t.VehicleEnrollmentId == requestVehicleEnrollment.Id); + + // Get route addresses of vehicle enrollment. + var ticketRouteAddresses = routeAddresses + .Where(e => e.RouteId == requestVehicleEnrollment.RouteId) + .OrderBy(e => e.Order); + + + // Count available capacity. + + // Get total capacity in requested vehicle. + int totalCapacity; + var vehicle = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid) + .Vehicle; + if (vehicle.VehicleType.Equals(VehicleType.Bus)) + { + totalCapacity = ((Bus)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Aircraft)) + { + totalCapacity = ((Aircraft)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Train)) + { + totalCapacity = ((Train)vehicle).Capacity; + } + else + { + throw new NotImplementedException(); + } + + int takenCapacity = 0; + + // For each bought ticket. + foreach (var ticket in tickets) + { + // Get departure and arrival route address + // of requested ticket. + var requestDepartureRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.DepartureRouteAddressGuid); + var requestArrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.ArrivalRouteAddressGuid); + + // Get departure and arrival route address + // of bought ticket. + var departureRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.DepartureRouteAddressId); + var arrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.ArrivalRouteAddressId); + + + // Count taken capacity in requested vehicle + // accounting for requested ticket + // departure and arrival route addresses. + // The algorithm is the same as vehicle enrollment + // time overlap check. + if ((requestDepartureRouteAddress.Order >= + departureRouteAddress.Order && + requestDepartureRouteAddress.Order < + arrivalRouteAddress.Order) || + (requestArrivalRouteAddress.Order <= + arrivalRouteAddress.Order && + requestArrivalRouteAddress.Order > + departureRouteAddress.Order) || + (requestDepartureRouteAddress.Order <= + departureRouteAddress.Order && + requestArrivalRouteAddress.Order >= + arrivalRouteAddress.Order)) + { + takenCapacity++; + } + } + + var availableCapacity = totalCapacity - takenCapacity; + + if (availableCapacity <= 0) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Calculate travel time and cost. + + var ticketsDetails = new List<(short order, DateTimeOffset departureTime, + DateTimeOffset arrivalTime, decimal cost, Currency currency)>(); + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id); + + var allRouteAddressDetails = (await _unitOfWork + .RouteAddressDetailRepository.GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress, + 1, int.MaxValue, cancellationToken)) + .Items; + + + foreach (var t in request.Tickets.OrderBy(t => t.Order)) + { + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + var departureRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid) + .Id; + var arrivalRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid) + .Id; + + var verad = allRouteAddressDetails + .Where(arad => arad.VehicleEnrollmentId == ve.Id) + .OrderBy(rad => rad.RouteAddress.Order) + .TakeWhile(rad => rad.Id != arrivalRouteAddressId); + + + // TODO: This counts departure address stop time which is + // not wrong but may be not desired. + var timeToDeparture = verad + .TakeWhile(rad => rad.Id != departureRouteAddressId) + .Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var departureTime = ve.DepartureTime.Add(timeToDeparture); + + + var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var arrivalTime = ve.DepartureTime.Add(timeToArrival); + + + var costToDeparture = verad + .TakeWhile(rad => rad.Id != departureRouteAddressId) + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var costToArrival = verad + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var cost = costToArrival - costToDeparture; + + + ticketsDetails.Add( + (t.Order, departureTime, arrivalTime, cost, ve.Currency)); + } + } + + // Check whether there are overlaps in ticket departure/arrival times. + { + for (int i = 1; i < ticketsDetails.Count; i++) + { + var previousTd = ticketsDetails[i - 1]; + var currentTd = ticketsDetails[i]; + + if (previousTd.arrivalTime >= currentTd.departureTime) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Create entity and insert into a datastore. + + var ticketsCostDetails = new List<(short order, + decimal cost, Currency currency)>(); + + TimeSpan travelTime; + + { + travelTime = + ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - + ticketsDetails.OrderBy(td => td.order).First().departureTime; + + foreach (var td in ticketsDetails) + { + var initialCurrency = td.currency; + var convertedCurrency = + _sessionCurrencyService.Currency != Currency.Default ? + _sessionCurrencyService.Currency : + initialCurrency; + + var cost = td.cost; + + var convertedCost = await _currencyConverterService + .ConvertAsync(cost, initialCurrency, + convertedCurrency, cancellationToken); + + ticketsCostDetails.Add((td.order, convertedCost, convertedCurrency)); + } + } + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeAddressGuids.Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var entity = new TicketGroup() + { + PassangerFirstName = request.PassangerFirstName, + PassangerLastName = request.PassangerLastName, + PassangerPatronymic = request.PassangerPatronymic, + PassangerSex = request.PassangerSex, + PassangerBirthDate = request.PassangerBirthDate, + PurchaseTime = request.PurchaseTime, + Returned = request.Returned, + TravelTime = travelTime, + Tickets = request.Tickets.Select( + t => + { + var ve = vehicleEnrollments.First( + ve => ve.Guid == t.VehicleEnrollmentGuid); + + + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + + var costDetail = ticketsCostDetails + .SingleOrDefault(td => td.order == t.Order); + + + return new Ticket() + { + DepartureRouteAddressId = departureRouteAddress.Id, + DepartureRouteAddress = departureRouteAddress, + ArrivalRouteAddressId = arrivalRouteAddress.Id, + ArrivalRouteAddress = arrivalRouteAddress, + Order = t.Order, + Cost = costDetail.cost, + Currency = costDetail.currency, + VehicleEnrollmentId = ve.Id + }; + }) + .ToArray() + }; + + entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs new file mode 100644 index 0000000..8ead4b4 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -0,0 +1,107 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public class AddTicketGroupCommandValidator : AbstractValidator +{ + public AddTicketGroupCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(tg => tg.PassangerFirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerLastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerPatronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerSex) + .Must((tg, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(tg => tg.PassangerBirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleFor(tg => tg.PurchaseTime) + .GreaterThanOrEqualTo(DateTimeOffset.UtcNow) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.UtcNow)); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.VehicleEnrollmentGuid) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.Order) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(tg => tg.Tickets).ChildRules(t => + { + t.RuleFor(t => t.DepartureRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.ArrivalRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.Order) + .GreaterThanOrEqualTo(short.MinValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + short.MinValue)) + .LessThanOrEqualTo(short.MaxValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + short.MaxValue)); + + t.RuleFor(t => t.VehicleEnrollmentGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + }); + } +} diff --git a/src/Application/TicketGroups/Models/TicketModel.cs b/src/Application/TicketGroups/Models/TicketModel.cs new file mode 100644 index 0000000..2cd2960 --- /dev/null +++ b/src/Application/TicketGroups/Models/TicketModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.Models; + +public sealed class TicketModel +{ + public Guid DepartureRouteAddressGuid { get; set; } + + public Guid ArrivalRouteAddressGuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentGuid { get; set; } +} diff --git a/src/Application/TicketGroups/TicketAddressDto.cs b/src/Application/TicketGroups/TicketAddressDto.cs new file mode 100644 index 0000000..d62e5c2 --- /dev/null +++ b/src/Application/TicketGroups/TicketAddressDto.cs @@ -0,0 +1,53 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketAddressDto : IMapFrom
+{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.City.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.City.Region.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.City.Name)); + } +} diff --git a/src/Application/TicketGroups/TicketDto.cs b/src/Application/TicketGroups/TicketDto.cs new file mode 100644 index 0000000..93194a5 --- /dev/null +++ b/src/Application/TicketGroups/TicketDto.cs @@ -0,0 +1,50 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public Guid DepartureRouteAddressUuid { get; set; } + + public Guid ArrivalRouteAddressUuid { get; set; } + + public TicketAddressDto DepartureAddress { get; set; } + + public TicketAddressDto ArrivalAddress { get; set; } + + public short Order { get; set; } + + public Guid VehicleEnrollmentUuid { get; set; } + + // TODO: Add VehicleEnrollment model + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.DepartureRouteAddressUuid, + opt => opt.MapFrom(s => s.DepartureRouteAddress.Guid)) + .ForMember( + d => d.ArrivalRouteAddressUuid, + opt => opt.MapFrom(s => s.ArrivalRouteAddress.Guid)) + .ForMember( + d => d.DepartureAddress, + opt => opt.MapFrom(s => s.DepartureRouteAddress.Address)) + .ForMember( + d => d.ArrivalAddress, + opt => opt.MapFrom(s => s.ArrivalRouteAddress.Address)) + .ForMember( + d => d.VehicleEnrollmentUuid, + opt => opt.MapFrom(s => s.VehicleEnrollment.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupDto.cs b/src/Application/TicketGroups/TicketGroupDto.cs new file mode 100644 index 0000000..ae9e0e8 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupDto.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public bool Returned { get; set; } + + public TimeSpan TravelTime { get; set; } + + + public ICollection Tickets { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.PassangerSex, + opt => opt.MapFrom(s => s.PassangerSex.Name)) + .ForMember( + d => d.PurchaseTime, + opt => opt + .MapFrom( + s => s.PurchaseTime)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs new file mode 100644 index 0000000..dd292ff --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class AddTicketGroupViewModel +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public bool Returned { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs new file mode 100644 index 0000000..82399f3 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class GetTicketGroupsPageFilterViewModel +{ + public string? Sex { get; set; } + + public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + + public Guid? CompanyUuid { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/TicketViewModel.cs b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs new file mode 100644 index 0000000..15560d1 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class TicketViewModel +{ + public Guid DepartureRouteAddressUuid { get; set; } + + public Guid ArrivalRouteAddressUuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentUuid { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs new file mode 100644 index 0000000..b58310e --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class UpdateTicketGroupViewModel +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public bool Returned { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs index 3f4dac9..9688d98 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -105,13 +105,13 @@ public class AddVehicleEnrollmentCommandHandler : // Three cases are included: // - // ---RD---------SD----------RA---> + // ---SD---------RD----------SA---> // time // - // ---RD---------SA----------RA---> + // ---SD---------RA----------SA---> // time // - // ---SD-----RD-------RA-----SA---> + // ---RD-----SD-------SA-----RA---> // time // Where: // RD - request enrollment departure time @@ -134,12 +134,12 @@ public class AddVehicleEnrollmentCommandHandler : rad.CurrentAddressStopTime); return - (departureTime >= requestDepartureTime && - departureTime <= requestArrivalTime) || - (arrivalTime >= requestDepartureTime && - arrivalTime <= requestArrivalTime) || - (departureTime <= requestDepartureTime && - arrivalTime >= requestArrivalTime); + (requestDepartureTime >= departureTime && + requestDepartureTime <= arrivalTime) || + (requestArrivalTime >= departureTime && + requestArrivalTime <= arrivalTime) || + (requestDepartureTime <= departureTime && + requestArrivalTime >= arrivalTime); }) .Any(); diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs index 1b2d0b7..d562c45 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -83,13 +83,13 @@ public class UpdateVehicleEnrollmentCommandHandler : // Three cases are included: // - // ---RD---------SD----------RA---> + // ---SD---------RD----------SA---> // time // - // ---RD---------SA----------RA---> + // ---SD---------RA----------SA---> // time // - // ---SD-----RD-------RA-----SA---> + // ---RD-----SD-------SA-----RA---> // time // Where: // RD - request enrollment departure time @@ -112,12 +112,12 @@ public class UpdateVehicleEnrollmentCommandHandler : rad.CurrentAddressStopTime); return - (departureTime >= requestDepartureTime && - departureTime <= requestArrivalTime) || - (arrivalTime >= requestDepartureTime && - arrivalTime <= requestArrivalTime) || - (departureTime <= requestDepartureTime && - arrivalTime >= requestArrivalTime); + (requestDepartureTime >= departureTime && + requestDepartureTime <= arrivalTime) || + (requestArrivalTime >= departureTime && + requestArrivalTime <= arrivalTime) || + (requestDepartureTime <= departureTime && + requestArrivalTime >= arrivalTime); }) .Any(); diff --git a/src/Domain/Entities/Ticket.cs b/src/Domain/Entities/Ticket.cs new file mode 100644 index 0000000..3865d77 --- /dev/null +++ b/src/Domain/Entities/Ticket.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Ticket : EntityBase +{ + public long DepartureRouteAddressId { get; set; } + + public RouteAddress DepartureRouteAddress { get; set; } + + public long ArrivalRouteAddressId { get; set; } + + public RouteAddress ArrivalRouteAddress { get; set; } + + public short Order { get; set; } + + public Currency Currency { get; set; } + + public decimal Cost { get; set; } + + + public long TicketGroupId { get; set; } + + public TicketGroup TicketGroup { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } +} diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs new file mode 100644 index 0000000..b044ceb --- /dev/null +++ b/src/Domain/Entities/TicketGroup.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class TicketGroup : EntityBase +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public bool Returned { get; set; } + + public TimeSpan TravelTime { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs new file mode 100644 index 0000000..59b8cb6 --- /dev/null +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -0,0 +1,221 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.TicketGroups; +using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; +// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; +// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; +// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.UpdateTicketGroup; +// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.DeleteTicketGroup; +using cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; +using cuqmbr.TravelGuide.Application.TicketGroups.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("ticketGroups")] +public class TicketGroupsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a ticketGroup")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(TicketGroupDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddTicketGroupViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddTicketGroupCommand() + { + PassangerFirstName = viewModel.PassangerFirstName, + PassangerLastName = viewModel.PassangerLastName, + PassangerPatronymic = viewModel.PassangerPatronymic, + PassangerSex = Sex.FromName(viewModel.PassangerSex), + PassangerBirthDate = viewModel.PassangerBirthDate, + PurchaseTime = viewModel.PurchaseTime, + Returned = viewModel.Returned, + Tickets = viewModel.Tickets.Select(e => + new TicketModel() + { + DepartureRouteAddressGuid = + e.DepartureRouteAddressUuid, + ArrivalRouteAddressGuid = + e.ArrivalRouteAddressUuid, + Order = e.Order, + VehicleEnrollmentGuid = + e.VehicleEnrollmentUuid, + }) + .ToArray() + }, + cancellationToken)); + } + + // [HttpGet] + // [SwaggerOperation("Get a list of all ticketGroups")] + // [SwaggerResponse( + // StatusCodes.Status200OK, "Request successful", + // typeof(PaginatedList))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task> GetPage( + // [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + // [FromQuery] SortQuery sortQuery, + // [FromQuery] GetTicketGroupsPageFilterViewModel filterQuery, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send( + // new GetTicketGroupsPageQuery() + // { + // PageNumber = pageQuery.PageNumber, + // PageSize = pageQuery.PageSize, + // Search = searchQuery.Search, + // Sort = sortQuery.Sort, + // LongitudeGreaterOrEqualThan = + // filterQuery.LongitudeGreaterOrEqualThan, + // LongitudeLessOrEqualThan = + // filterQuery.LongitudeLessOrEqualThan, + // LatitudeGreaterOrEqualThan = + // filterQuery.LatitudeGreaterOrEqualThan, + // LatitudeLessOrEqualThan = + // filterQuery.LatitudeLessOrEqualThan, + // VehicleType = VehicleType.FromName(filterQuery.VehicleType), + // CountryGuid = filterQuery.CountryUuid, + // RegionGuid = filterQuery.RegionUuid, + // CityGuid = filterQuery.CityUuid + // }, + // cancellationToken); + // } + // + // [HttpGet("{uuid:guid}")] + // [SwaggerOperation("Get a ticketGroup by uuid")] + // [SwaggerResponse( + // StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", typeof(TicketGroupDto))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Get( + // [FromRoute] Guid uuid, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send(new GetTicketGroupQuery() { Guid = uuid }, + // cancellationToken); + // } + // + // [HttpPut("{uuid:guid}")] + // [SwaggerOperation("Update a ticketGroup")] + // [SwaggerResponse( + // StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Object already exists", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", typeof(TicketGroupDto))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Parent object not found", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Update( + // [FromRoute] Guid uuid, + // [FromBody] UpdateTicketGroupViewModel viewModel, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send( + // new UpdateTicketGroupCommand() + // { + // Guid = uuid, + // Name = viewModel.Name, + // Longitude = viewModel.Longitude, + // Latitude = viewModel.Latitude, + // VehicleType = VehicleType.FromName(viewModel.VehicleType), + // CityGuid = viewModel.CityUuid + // }, + // cancellationToken); + // } + // + // [HttpDelete("{uuid:guid}")] + // [SwaggerOperation("Delete a ticketGroup")] + // [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Delete( + // [FromRoute] Guid uuid, + // CancellationToken cancellationToken) + // { + // await Mediator.Send( + // new DeleteTicketGroupCommand() { Guid = uuid }, + // cancellationToken); + // return StatusCode(StatusCodes.Status204NoContent); + // } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index e27b3ba..3dc85f8 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; using cuqmbr.TravelGuide.Persistence.TypeConverters; @@ -12,6 +13,15 @@ public class InMemoryDbContext : DbContext protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + + builder + .Entity() + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); } protected override void ConfigureConventions( @@ -19,12 +29,26 @@ public class InMemoryDbContext : DbContext { builder .Properties() - .HaveColumnType("vehicle_type") + .HaveColumnType("varchar(16)") .HaveConversion(); builder .Properties() - .HaveColumnType("currency") + .HaveColumnType("varchar(8)") .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveConversion(); } } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index f7f1d15..4b257ff 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -28,6 +28,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork new InMemoryRouteAddressRepository(_dbContext); CompanyRepository = new InMemoryCompanyRepository(_dbContext); EmployeeRepository = new InMemoryEmployeeRepository(_dbContext); + TicketGroupRepository = new InMemoryTicketGroupRepository(_dbContext); + TicketRepository = new InMemoryTicketRepository(_dbContext); + RouteAddressDetailRepository = + new InMemoryRouteAddressDetailRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -56,6 +60,12 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public EmployeeRepository EmployeeRepository { get; init; } + public TicketGroupRepository TicketGroupRepository { get; init; } + + public TicketRepository TicketRepository { get; init; } + + public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs new file mode 100644 index 0000000..992e9a1 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteAddressDetailRepository : + InMemoryBaseRepository, RouteAddressDetailRepository +{ + public InMemoryRouteAddressDetailRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs new file mode 100644 index 0000000..613715b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTicketGroupRepository : + InMemoryBaseRepository, TicketGroupRepository +{ + public InMemoryTicketGroupRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs new file mode 100644 index 0000000..ab406b0 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTicketRepository : + InMemoryBaseRepository, TicketRepository +{ + public InMemoryTicketRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs new file mode 100644 index 0000000..2b2771d --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs @@ -0,0 +1,105 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class TicketConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(t => t.Currency) + .HasColumnName("currency") + .IsRequired(true); + + builder + .ToTable( + "tickets", + ve => ve.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()}", + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Currency.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(t => t.DepartureRouteAddressId) + .HasColumnName("departure_route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .Property(t => t.ArrivalRouteAddressId) + .HasColumnName("arrival_route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .Property(t => t.Order) + .HasColumnName("order") + .HasColumnType("smallint") + .IsRequired(true); + + builder + .Property(t => t.Cost) + .HasColumnName("cost") + .HasColumnType("numeric(24,12)") + .IsRequired(true); + + + builder + .Property(t => t.TicketGroupId) + .HasColumnName("ticket_group_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(t => t.TicketGroup) + .WithMany(tg => tg.Tickets) + .HasForeignKey(t => t.TicketGroupId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(t => t.TicketGroupId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}"); + + + builder + .Property(t => t.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(t => t.VehicleEnrollment) + .WithMany(ve => ve.Tickets) + .HasForeignKey(t => t.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(t => t.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs new file mode 100644 index 0000000..9bc2947 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -0,0 +1,75 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class TicketGroupConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(tg => tg.PassangerSex) + .HasColumnName("passanger_sex") + .IsRequired(true); + + builder + .ToTable( + "ticket_groups", + tg => tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.PassangerSex) + .Metadata.GetColumnName()}", + $"{builder.Property(g => g.PassangerSex) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(a => a.PassangerFirstName) + .HasColumnName("passanger_first_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(a => a.PassangerLastName) + .HasColumnName("passanger_last_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(a => a.PassangerPatronymic) + .HasColumnName("passanger_patronymic") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(a => a.PassangerBirthDate) + .HasColumnName("passanger_birth_date") + .HasColumnType("date") + .IsRequired(true); + + builder + .Property(a => a.PurchaseTime) + .HasColumnName("purchase_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(a => a.Returned) + .HasColumnName("returned") + .HasColumnType("boolean") + .IsRequired(true); + + builder + .Property(a => a.TravelTime) + .HasColumnName("travel_time") + .HasColumnType("interval") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs new file mode 100644 index 0000000..37b26bc --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs @@ -0,0 +1,1016 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250519212941_Add_Ticket_and_TicketGroup")] + partial class Add_Ticket_and_TicketGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Returned") + .HasColumnType("boolean") + .HasColumnName("returned"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs new file mode 100644 index 0000000..6cbe97e --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Ticket_and_TicketGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "ticket_groups_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "tickets_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "ticket_groups", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.ticket_groups_id_sequence')"), + passanger_first_name = table.Column(type: "varchar(32)", nullable: false), + passanger_last_name = table.Column(type: "varchar(32)", nullable: false), + passanger_patronymic = table.Column(type: "varchar(32)", nullable: false), + passanger_sex = table.Column(type: "varchar(32)", nullable: false), + passanger_birth_date = table.Column(type: "date", nullable: false), + purchase_time = table.Column(type: "timestamptz", nullable: false), + returned = table.Column(type: "boolean", nullable: false), + travel_time = table.Column(type: "interval", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_ticket_groups", x => x.id); + table.UniqueConstraint("altk_ticket_groups_uuid", x => x.uuid); + table.CheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + }); + + migrationBuilder.CreateTable( + name: "tickets", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.tickets_id_sequence')"), + departure_route_address_id = table.Column(type: "bigint", nullable: false), + arrival_route_address_id = table.Column(type: "bigint", nullable: false), + order = table.Column(type: "smallint", nullable: false), + currency = table.Column(type: "varchar(8)", nullable: false), + cost = table.Column(type: "numeric(24,12)", nullable: false), + ticket_group_id = table.Column(type: "bigint", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tickets", x => x.id); + table.UniqueConstraint("altk_tickets_uuid", x => x.uuid); + table.CheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + table.ForeignKey( + name: "FK_tickets_route_addresses_arrival_route_address_id", + column: x => x.arrival_route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_tickets_route_addresses_departure_route_address_id", + column: x => x.departure_route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tickets_ticket_group_id", + column: x => x.ticket_group_id, + principalSchema: "application", + principalTable: "ticket_groups", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tickets_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_tickets_arrival_route_address_id", + schema: "application", + table: "tickets", + column: "arrival_route_address_id"); + + migrationBuilder.CreateIndex( + name: "IX_tickets_departure_route_address_id", + schema: "application", + table: "tickets", + column: "departure_route_address_id"); + + migrationBuilder.CreateIndex( + name: "ix_tickets_ticket_group_id", + schema: "application", + table: "tickets", + column: "ticket_group_id"); + + migrationBuilder.CreateIndex( + name: "ix_tickets_vehicle_enrollment_id", + schema: "application", + table: "tickets", + column: "vehicle_enrollment_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tickets", + schema: "application"); + + migrationBuilder.DropTable( + name: "ticket_groups", + schema: "application"); + + migrationBuilder.DropSequence( + name: "ticket_groups_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "tickets_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index c2eb06f..fb4e67f 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -43,6 +43,10 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); modelBuilder.HasSequence("vehicles_id_sequence"); @@ -466,6 +470,133 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("route_address_details", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Returned") + .HasColumnType("boolean") + .HasColumnName("returned"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => { b.Property("Id") @@ -746,6 +877,43 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("VehicleEnrollment"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") @@ -823,6 +991,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Details"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => { b.Navigation("Enrollments"); @@ -831,6 +1004,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => { b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); }); #pragma warning restore 612, 618 } diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 46ff3a9..9b324a2 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -28,6 +28,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork new PostgreSqlRouteAddressRepository(_dbContext); CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); EmployeeRepository = new PostgreSqlEmployeeRepository(_dbContext); + TicketGroupRepository = new PostgreSqlTicketGroupRepository(_dbContext); + TicketRepository = new PostgreSqlTicketRepository(_dbContext); + RouteAddressDetailRepository = + new PostgreSqlRouteAddressDetailRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -56,6 +60,12 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public EmployeeRepository EmployeeRepository { get; init; } + public TicketGroupRepository TicketGroupRepository { get; init; } + + public TicketRepository TicketRepository { get; init; } + + public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs new file mode 100644 index 0000000..d88305f --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteAddressDetailRepository : + PostgreSqlBaseRepository, RouteAddressDetailRepository +{ + public PostgreSqlRouteAddressDetailRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs new file mode 100644 index 0000000..204d808 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTicketGroupRepository : + PostgreSqlBaseRepository, TicketGroupRepository +{ + public PostgreSqlTicketGroupRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs new file mode 100644 index 0000000..b2f53f6 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTicketRepository : + PostgreSqlBaseRepository, TicketRepository +{ + public PostgreSqlTicketRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} From d5ffedbdb9f92dfdcdf5e6ce96c7994c9f89fb8d Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 23 May 2025 14:19:47 +0300 Subject: [PATCH 17/35] add shortest vehicle enrollment with transfers search --- src/Application/Application.csproj | 1 + src/Application/Routes/RouteAddressDto.cs | 20 +- .../SearchShortest/SearchShortestQuery.cs | 21 + .../SearchShortestQueryAuthorizer.cs | 33 ++ .../SearchShortestQueryHandler.cs | 436 ++++++++++++++++++ .../SearchShortestQueryValidator.cs | 43 ++ .../VehicleEnrollmentSearchAddressDto.cs | 34 ++ .../VehicleEnrollmentSearchDto.cs | 17 + ...cleEnrollmentSearchVehicleEnrollmentDto.cs | 29 ++ .../ViewModels/SearchShortestViewModel.cs | 16 + src/Application/packages.lock.json | 6 + src/Configuration/packages.lock.json | 6 + src/Domain/Entities/VehicleEnrollment.cs | 193 ++++++++ .../VehicleEnrollmentSearchController.cs | 54 +++ src/HttpApi/packages.lock.json | 6 + src/Identity/packages.lock.json | 6 + .../ExchangeApiCurrencyConverterService.cs | 2 +- src/Infrastructure/packages.lock.json | 6 + src/Persistence/packages.lock.json | 6 + .../packages.lock.json | 6 + 20 files changed, 930 insertions(+), 11 deletions(-) create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs create mode 100644 src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs create mode 100644 src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs create mode 100644 src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs create mode 100644 src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs create mode 100644 src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index e77ed7a..9b7a24d 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Application/Routes/RouteAddressDto.cs b/src/Application/Routes/RouteAddressDto.cs index fbe2148..8fcd4ec 100644 --- a/src/Application/Routes/RouteAddressDto.cs +++ b/src/Application/Routes/RouteAddressDto.cs @@ -10,15 +10,15 @@ public sealed class RouteAddressDto : IMapFrom public short Order { get; set; } - public Guid AddressUuid { get; set; } + public Guid Uuid { get; set; } - public string AddressName { get; set; } + public string Name { get; set; } - public double AddressLongitude { get; set; } + public double Longitude { get; set; } - public double AddressLatitude { get; set; } + public double Latitude { get; set; } - public string AddressVehicleType { get; set; } + public string VehicleType { get; set; } public Guid CountryUuid { get; set; } @@ -39,19 +39,19 @@ public sealed class RouteAddressDto : IMapFrom d => d.RouteAddressUuid, opt => opt.MapFrom(s => s.Guid)) .ForMember( - d => d.AddressUuid, + d => d.Uuid, opt => opt.MapFrom(s => s.Address.Guid)) .ForMember( - d => d.AddressName, + d => d.Name, opt => opt.MapFrom(s => s.Address.Name)) .ForMember( - d => d.AddressLongitude, + d => d.Longitude, opt => opt.MapFrom(s => s.Address.Longitude)) .ForMember( - d => d.AddressLatitude, + d => d.Latitude, opt => opt.MapFrom(s => s.Address.Latitude)) .ForMember( - d => d.AddressVehicleType, + d => d.VehicleType, opt => opt.MapFrom(s => s.Address.VehicleType.Name)) .ForMember( d => d.CityUuid, diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs new file mode 100644 index 0000000..4bdfae6 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public record SearchShortestQuery : + IRequest +{ + public Guid DepartureAddressGuid { get; set; } + + public Guid ArrivalAddressGuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public bool ShortestByCost { get; set; } + + public bool ShortestByTime { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs new file mode 100644 index 0000000..97eda14 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public class SearchShortestQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public SearchShortestQueryAuthorizer( + SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(SearchShortestQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs new file mode 100644 index 0000000..5c5f0b3 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -0,0 +1,436 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using QuikGraph; +using QuikGraph.Algorithms; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +// TODO: Refactor. +// TODO: Add configurable time between transfers. +public class SearchShortestQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + public SearchShortestQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + } + + public async Task Handle( + SearchShortestQuery request, + CancellationToken cancellationToken) + { + // Get related data + + var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero); + var departureDate = + new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero); + + var range = TimeSpan.FromDays(3); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + e.DepartureTime >= departureDate.Subtract(range) && + e.DepartureTime <= departureDate.Add(range) && + request.VehicleTypes.Contains(e.Vehicle.VehicleType), + e => e.Route, + 1, int.MaxValue, cancellationToken)) + .Items; + + if (vehicleEnrollments.Count == 0) + { + throw new NotFoundException(); + } + + var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress.Address.City.Region.Country, + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with route address details + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + vehicleEnrollment.RouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order) + .ToArray(); + } + + + // Creat and fill graph data structure + + var graph = new AdjacencyGraph< + Address, TaggedEdge>(); + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + var vehicleEnrollmentRouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order); + + for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++) + { + var sourceRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i-1); + var targetRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i); + + var sourceAddress = + sourceRouteAddressDetail.RouteAddress.Address; + var targetAddress = + targetRouteAddressDetail.RouteAddress.Address; + + var weight = sourceRouteAddressDetail.CostToNextAddress; + + graph.AddVerticesAndEdge( + new TaggedEdge( + sourceAddress, targetAddress, sourceRouteAddressDetail)); + } + } + + + // Find paths + + var departureAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) + .RouteAddress.Address; + var arrivalAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) + .RouteAddress.Address; + + + Func, double> weightByCost = + edge => (double)edge.Tag.CostToNextAddress; + + Func, double> weightByTime = + edge => + edge.Tag.TimeToNextAddress.Ticks + + edge.Tag.CurrentAddressStopTime.Ticks; + + Func, double> edgeWeight = + _ => 0; + + if (request.ShortestByCost && request.ShortestByTime) + { + edgeWeight = edge => weightByCost(edge) + weightByTime(edge); + } + else if (request.ShortestByCost) + { + edgeWeight = edge => weightByCost(edge); + } + else if (request.ShortestByTime) + { + edgeWeight = edge => weightByTime(edge); + } + + + var tryGetPaths = graph.ShortestPathsDijkstra(edgeWeight, departureAddress); + + + // Create and hydrate a DTO object + + var vehicleEnrollmentDtos = + new List(); + + var totalTravelTime = TimeSpan.Zero; + var totalCost = (decimal)0; + + if (tryGetPaths(arrivalAddress, out var path)) + { + var firstRouteAddressId = path.First().Tag.RouteAddressId; + long lastRouteAddressId; + Guid lastRouteAddressGuid; + + var addressDtos = new List(); + var addressOrder = (short)1; + var enrollmentTravelTime = TimeSpan.Zero; + var enrollmentCost = (decimal)0; + var enrollmentOrder = (short)1; + + Address source; + Address target; + RouteAddressDetail tag; + RouteAddressDetail nextTag; + + for (int i = 0; i < path.Count() - 1; i++) + { + source = path.Select(e => e.Source).ElementAt(i); + tag = path.Select(e => e.Tag).ElementAt(i); + nextTag = path.Select(e => e.Tag).ElementAt(i+1); + + + totalTravelTime += + tag.TimeToNextAddress + tag.CurrentAddressStopTime; + enrollmentTravelTime += + tag.TimeToNextAddress + tag.CurrentAddressStopTime; + + totalCost += tag.CostToNextAddress; + enrollmentCost += tag.CostToNextAddress; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = tag.CostToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + + // First address after transfer + if (nextTag.VehicleEnrollmentId != tag.VehicleEnrollmentId) + { + target = path.Select(e => e.Target).ElementAt(i); + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = tag.VehicleEnrollment.Currency.Name, + Cost = tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + + firstRouteAddressId = nextTag.RouteAddressId; + + addressDtos = new List(); + addressOrder = (short)1; + enrollmentTravelTime = TimeSpan.Zero; + enrollmentCost = (decimal)0; + enrollmentOrder++; + } + } + + source = path.Select(e => e.Source).Last(); + target = path.Select(e => e.Target).Last(); + tag = path.Select(e => e.Tag).Last(); + nextTag = path.Select(e => e.Tag).Last(); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = tag.CostToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressOrder++; + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = tag.VehicleEnrollment.Currency.Name, + Cost = tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + } + else + { + throw new NotFoundException(); + } + + + foreach (var vehicleEnrollmentDto in vehicleEnrollmentDtos) + { + foreach (var addressDto in vehicleEnrollmentDto.Addresses) + { + addressDto.CostToNextAddress = await _currencyConverterService + .ConvertAsync(addressDto.CostToNextAddress, + vehicleEnrollments + .First(e => e.Guid == vehicleEnrollmentDto.Uuid) + .Currency, + _sessionCurrencyService.Currency, cancellationToken); + } + + vehicleEnrollmentDto.Currency = vehicleEnrollmentDto.Currency; + vehicleEnrollmentDto.Cost = vehicleEnrollmentDto.Addresses + .Aggregate((decimal)0, + (sum, next) => sum += next.CostToNextAddress); + } + + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, + (sum, next) => sum += next.Cost); + + var departureTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).First().DepartureTime; + var arrivalTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).Last().ArrivalTime; + var timeInStops = vehicleEnrollmentDtos + .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); + var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + + return new VehicleEnrollmentSearchDto() + { + DepartureTime = departureTime, + ArrivalTime = arrivalTime, + TravelTime = arrivalTime - departureTime, + TimeInStops = timeInStops, + NumberOfTransfers = numberOfTransfers, + Enrollments = vehicleEnrollmentDtos + }; + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs new file mode 100644 index 0000000..19bb4c4 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public class SearchShortestQueryValidator : + AbstractValidator +{ + public SearchShortestQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.DepartureAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.ArrivalAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureDate) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow))); + + RuleForEach(v => v.VehicleTypes) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs new file mode 100644 index 0000000..01b2602 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs @@ -0,0 +1,34 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchAddressDto +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public short Order { get; set; } + + public Guid RouteAddressUuid { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs new file mode 100644 index 0000000..2409797 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs @@ -0,0 +1,17 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchDto +{ + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfTransfers { get; set; } + + public ICollection + Enrollments { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs new file mode 100644 index 0000000..5481f76 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs @@ -0,0 +1,29 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchVehicleEnrollmentDto +{ + public string VehicleType { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeMoving { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfStops { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public Guid Uuid { get; set; } + + public short Order { get; set; } + + public ICollection + Addresses { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs new file mode 100644 index 0000000..aa9813a --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +public sealed class SearchShortestViewModel +{ + public Guid DepartureAddressUuid { get; set; } + + public Guid ArrivalAddressUuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public bool ShortestByCost { get; set; } + + public bool ShortestByTime { get; set; } +} diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index f56b518..ac19a92 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -59,6 +59,12 @@ "Microsoft.Extensions.Options": "9.0.4" } }, + "QuikGraph": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "System.Linq.Dynamic.Core": { "type": "Direct", "requested": "[1.6.2, )", diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index 47118f9..8fb4534 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -742,6 +742,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -838,6 +843,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs index 31ae842..b6c2ca0 100644 --- a/src/Domain/Entities/VehicleEnrollment.cs +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -23,4 +23,197 @@ public class VehicleEnrollment : EntityBase public ICollection Tickets { get; set; } + + + public DateTimeOffset GetDepartureTime(long DepartureRouteAddressId) + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + + if (DepartureRouteAddressId == firstRouteAddressId) + { + return DepartureTime; + } + + + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var timeToDeparture = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + timeToDeparture = + timeToDeparture + routeAddressDetail.CurrentAddressStopTime; + + if (routeAddressDetail.Id == DepartureRouteAddressId) + { + break; + } + + timeToDeparture = + timeToDeparture += routeAddressDetail.TimeToNextAddress; + } + + return DepartureTime + timeToDeparture; + } + + public DateTimeOffset GetDepartureTime() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + + return GetDepartureTime(firstRouteAddressId); + } + + public DateTimeOffset GetArrivalTime(long ArrivalRouteAddressId) + { + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var timeToDeparture = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + if (routeAddressDetail.Id == ArrivalRouteAddressId) + { + break; + } + + timeToDeparture = + timeToDeparture + + routeAddressDetail.TimeToNextAddress + + routeAddressDetail.CurrentAddressStopTime; + } + + return DepartureTime + timeToDeparture; + } + + public DateTimeOffset GetArrivalTime() + { + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetArrivalTime(lastRouteAddressId); + } + + public TimeSpan GetTravelTime( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + GetArrivalTime(ArrivalRouteAddressId) - + GetDepartureTime(DepartureRouteAddressId); + } + + public TimeSpan GetTravelTime() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTravelTime(firstRouteAddressId, lastRouteAddressId); + } + + public TimeSpan GetTimeInStops( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var departureRouteAddressDetail = orderedRouteAddressDetails + .Single(e => e.Id == DepartureRouteAddressId); + + var timeInStops = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + if (routeAddressDetail.RouteAddress.Order <= + departureRouteAddressDetail.RouteAddress.Order) + { + continue; + } + + if (routeAddressDetail.Id == ArrivalRouteAddressId) + { + break; + } + + timeInStops += routeAddressDetail.CurrentAddressStopTime; + } + + return timeInStops; + } + + public TimeSpan GetTimeInStops() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeInStops(firstRouteAddressId, lastRouteAddressId); + } + + public int GetNumberOfStops( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.Id != DepartureRouteAddressId) + .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .Count() - 1; + } + + public TimeSpan GetNumberOfStops() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeInStops(firstRouteAddressId, lastRouteAddressId); + } + + public TimeSpan GetTimeMoving( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.Id != DepartureRouteAddressId) + .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .Aggregate(TimeSpan.Zero, + (sum, next) => sum += next.TimeToNextAddress); + } + + public TimeSpan GetTimeMoving() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeMoving(firstRouteAddressId, lastRouteAddressId); + } + + public decimal GetCost( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.Id != DepartureRouteAddressId) + .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .Aggregate((decimal)0, + (sum, next) => sum += next.CostToNextAddress); + } + + public decimal GetCost() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetCost(firstRouteAddressId, lastRouteAddressId); + } } diff --git a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs new file mode 100644 index 0000000..e10020e --- /dev/null +++ b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch + .Queries.SearchShortest; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("vehicleEnrollmentSearch")] +public class VehicleEnrollmentSearchController : ControllerBase +{ + [HttpGet] + [SwaggerOperation("Search vehicle enrollments with transfers")] + [SwaggerResponse( + StatusCodes.Status200OK, "Search successful", + typeof(VehicleEnrollmentSearchDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "No enrollments found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromQuery] SearchShortestViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new SearchShortestQuery() + { + DepartureAddressGuid = viewModel.DepartureAddressUuid, + ArrivalAddressGuid = viewModel.ArrivalAddressUuid, + DepartureDate = viewModel.DepartureDate, + VehicleTypes = viewModel.VehicleTypes + .Select(e => VehicleType.FromName(e)).ToHashSet(), + ShortestByCost = viewModel.ShortestByCost, + ShortestByTime = viewModel.ShortestByTime + }, + cancellationToken)); + } +} diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index a8c38fc..58c6c3e 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -897,6 +897,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -1079,6 +1084,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json index aa4f436..22ffc53 100644 --- a/src/Identity/packages.lock.json +++ b/src/Identity/packages.lock.json @@ -528,6 +528,11 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "System.Buffers": { "type": "Transitive", "resolved": "4.6.0", @@ -589,6 +594,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs index 15d444d..ee4d9d0 100644 --- a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -41,7 +41,7 @@ public sealed class ExchangeApiCurrencyConverterService : public async Task ConvertAsync(decimal amount, Currency from, Currency to, DateTimeOffset time, CancellationToken cancellationToken) { - if (from.Equals(to)) + if (from.Equals(to) || to.Equals(Currency.Default)) { return amount; } diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index b89053f..f67a272 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -234,6 +234,11 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "System.Linq.Dynamic.Core": { "type": "Transitive", "resolved": "1.6.2", @@ -249,6 +254,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index af85452..3cb01b7 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -274,6 +274,11 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -329,6 +334,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 4fd295d..4967b9d 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -816,6 +816,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -982,6 +987,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, From e3dd2dd58233720a65b30c09ed6c319c662412a8 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sat, 24 May 2025 11:17:43 +0300 Subject: [PATCH 18/35] add all vehicle enrollment with transfers search --- .../Queries/SearchAll/SearchAllQuery.cs | 39 ++ .../SearchAll/SearchAllQueryAuthorizer.cs | 33 ++ .../SearchAll/SearchAllQueryHandler.cs | 522 ++++++++++++++++++ .../SearchAll/SearchAllQueryValidator.cs | 43 ++ .../SearchShortestQueryHandler.cs | 126 +++-- .../VehicleEnrollmentSearchDto.cs | 4 + .../ViewModels/SearchAllViewModel.cs | 32 ++ .../VehicleEnrollmentSearchController.cs | 71 ++- 8 files changed, 815 insertions(+), 55 deletions(-) create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs create mode 100644 src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs new file mode 100644 index 0000000..5f71e49 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public record SearchAllQuery : + IRequest> +{ + public Guid DepartureAddressGuid { get; set; } + + public Guid ArrivalAddressGuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public string Sort { get; set; } = String.Empty; + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs new file mode 100644 index 0000000..19ba695 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public SearchAllQueryAuthorizer( + SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(SearchAllQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs new file mode 100644 index 0000000..4cd7fa8 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -0,0 +1,522 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using QuikGraph; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +// TODO: Add configurable time between transfers. +public class SearchAllQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + + public SearchAllQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task> Handle( + SearchAllQuery request, + CancellationToken cancellationToken) + { + // Get related data + + var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero); + var departureDate = + new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero); + + var range = TimeSpan.FromDays(3); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + e.DepartureTime >= departureDate.Subtract(range) && + e.DepartureTime <= departureDate.Add(range) && + request.VehicleTypes.Contains(e.Vehicle.VehicleType), + e => e.Route, + 1, int.MaxValue, cancellationToken)) + .Items; + + if (vehicleEnrollments.Count == 0) + { + throw new NotFoundException(); + } + + var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress.Address.City.Region.Country, + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with route address details + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + vehicleEnrollment.RouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order) + .ToArray(); + } + + + // Creat and fill graph data structure + + var graph = new AdjacencyGraph< + Address, TaggedEdge>(); + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + var vehicleEnrollmentRouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order); + + for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++) + { + var sourceRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i - 1); + var targetRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i); + + var sourceAddress = + sourceRouteAddressDetail.RouteAddress.Address; + var targetAddress = + targetRouteAddressDetail.RouteAddress.Address; + + var weight = sourceRouteAddressDetail.CostToNextAddress; + + graph.AddVerticesAndEdge( + new TaggedEdge( + sourceAddress, targetAddress, sourceRouteAddressDetail)); + } + } + + + // Find paths + + var departureAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) + .RouteAddress.Address; + var arrivalAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) + .RouteAddress.Address; + + + var paths = new List>>(); + var queue = new Queue<(TaggedEdge edge, List> path)>(); + + foreach (var edge in graph.OutEdges(departureAddress)) + { + queue.Enqueue((edge, new List>() { edge })); + } + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + if (current.edge.Target.Equals(arrivalAddress)) + { + paths.Add(current.path); + continue; + } + + foreach (var edge in graph.OutEdges(current.edge.Target)) + { + var neighbor = edge; + if (!current.path.Contains(neighbor)) + { + var newPath = new List>(current.path) { neighbor }; + queue.Enqueue((neighbor, newPath)); + } + } + } + + + // Create DTO object + + var result = new List(); + + foreach (var path in paths) + { + var vehicleEnrollmentDtos = + new List(); + + var addressDtos = new List(); + + var firstRouteAddressId = path.First().Tag.RouteAddressId; + + Guid lastRouteAddressGuid; + long lastRouteAddressId; + + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + + short addressOrder = 1; + short enrollmentOrder = 1; + + Address source; + Address nextSource; + + Address target; + Address nextTarget; + + RouteAddressDetail tag; + RouteAddressDetail nextTag; + + for (int i = 0; i < path.Count - 1; i++) + { + var edge = path[i]; + var nextEdge = path[i+1]; + + source = edge.Source; + nextSource = nextEdge.Source; + + tag = edge.Tag; + nextTag = nextEdge.Tag; + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = costToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + + if (tag.VehicleEnrollmentId != nextTag.VehicleEnrollmentId) + { + target = edge.Target; + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + + + firstRouteAddressId = nextTag.RouteAddressId; + addressDtos = new List(); + addressOrder = 1; + enrollmentOrder++; + } + } + + // --------------- + + source = path.Select(e => e.Source).Last(); + target = path.Select(e => e.Target).Last(); + tag = path.Select(e => e.Tag).Last(); + + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = 0, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + + // --------------- + + + var departureTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).First().DepartureTime; + var arrivalTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).Last().ArrivalTime; + var timeInStops = vehicleEnrollmentDtos + .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); + var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); + + result.Add(new VehicleEnrollmentSearchDto() + { + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = arrivalTime - departureTime, + TimeInStops = timeInStops, + NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, + Enrollments = vehicleEnrollmentDtos + }); + } + + + if (result.Count == 0) + { + throw new NotFoundException(); + } + + + var filteredResult = result.Where(e => + (request.TravelTimeGreaterThanOrEqualTo != null + ? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo + : true) && + (request.TravelTimeLessThanOrEqualTo != null + ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true) && + (request.CostGreaterThanOrEqualTo != null + ? e.Cost >= request.CostGreaterThanOrEqualTo + : true) && + (request.CostLessThanOrEqualTo != null + ? e.Cost <= request.CostLessThanOrEqualTo + : true) && + (request.NumberOfTransfersGreaterThanOrEqualTo != null + ? e.NumberOfTransfers >= request.NumberOfTransfersGreaterThanOrEqualTo + : true) && + (request.NumberOfTransfersLessThanOrEqualTo != null + ? e.NumberOfTransfers <= request.NumberOfTransfersLessThanOrEqualTo + : true) && + (request.DepartureTimeGreaterThanOrEqualTo != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqualTo + : true) && + (request.DepartureTimeLessThanOrEqualTo != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqualTo + : true) && + (request.ArrivalTimeGreaterThanOrEqualTo != null + ? e.ArrivalTime >= request.ArrivalTimeGreaterThanOrEqualTo + : true) && + (request.ArrivalTimeLessThanOrEqualTo != null + ? e.ArrivalTime <= request.ArrivalTimeLessThanOrEqualTo + : true)); + + var sortedResult = QueryableExtension + .ApplySort(filteredResult.AsQueryable(), request.Sort); + + + return sortedResult; + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs new file mode 100644 index 0000000..e12f958 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryValidator : + AbstractValidator +{ + public SearchAllQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.DepartureAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.ArrivalAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureDate) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow))); + + RuleForEach(v => v.VehicleTypes) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs index 5c5f0b3..c1acabb 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -6,11 +6,11 @@ using AutoMapper; using QuikGraph; using QuikGraph.Algorithms; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application .VehicleEnrollmentSearch.Queries.SearchShortest; -// TODO: Refactor. // TODO: Add configurable time between transfers. public class SearchShortestQueryHandler : IRequestHandler @@ -21,16 +21,20 @@ public class SearchShortestQueryHandler : private readonly SessionCurrencyService _sessionCurrencyService; private readonly CurrencyConverterService _currencyConverterService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + public SearchShortestQueryHandler( UnitOfWork unitOfWork, IMapper mapper, SessionCurrencyService sessionCurrencyService, - CurrencyConverterService currencyConverterService) + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) { _unitOfWork = unitOfWork; _mapper = mapper; _sessionCurrencyService = sessionCurrencyService; _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; } public async Task Handle( @@ -163,10 +167,14 @@ public class SearchShortestQueryHandler : long lastRouteAddressId; Guid lastRouteAddressGuid; + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + var addressDtos = new List(); + var addressOrder = (short)1; - var enrollmentTravelTime = TimeSpan.Zero; - var enrollmentCost = (decimal)0; var enrollmentOrder = (short)1; Address source; @@ -181,13 +189,10 @@ public class SearchShortestQueryHandler : nextTag = path.Select(e => e.Tag).ElementAt(i+1); - totalTravelTime += - tag.TimeToNextAddress + tag.CurrentAddressStopTime; - enrollmentTravelTime += - tag.TimeToNextAddress + tag.CurrentAddressStopTime; - - totalCost += tag.CostToNextAddress; - enrollmentCost += tag.CostToNextAddress; + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { @@ -202,7 +207,7 @@ public class SearchShortestQueryHandler : CityUuid = source.City.Guid, CityName = source.City.Name, TimeToNextAddress = tag.TimeToNextAddress, - CostToNextAddress = tag.CostToNextAddress, + CostToNextAddress = costToNextAddress, CurrentAddressStopTime = tag.CurrentAddressStopTime, Order = addressOrder, RouteAddressUuid = tag.RouteAddress.Guid @@ -255,13 +260,29 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Id; + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + vehicleEnrollmentDtos.Add( new VehicleEnrollmentSearchVehicleEnrollmentDto() { DepartureTime = tag.VehicleEnrollment - .GetDepartureTime(firstRouteAddressId), + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), ArrivalTime = tag.VehicleEnrollment - .GetArrivalTime(lastRouteAddressId), + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = tag.VehicleEnrollment .GetTravelTime(firstRouteAddressId, lastRouteAddressId), @@ -274,10 +295,8 @@ public class SearchShortestQueryHandler : NumberOfStops = tag.VehicleEnrollment .GetNumberOfStops(firstRouteAddressId, lastRouteAddressId), - Currency = tag.VehicleEnrollment.Currency.Name, - Cost = tag.VehicleEnrollment - .GetCost(firstRouteAddressId, - lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, @@ -288,8 +307,6 @@ public class SearchShortestQueryHandler : addressDtos = new List(); addressOrder = (short)1; - enrollmentTravelTime = TimeSpan.Zero; - enrollmentCost = (decimal)0; enrollmentOrder++; } } @@ -299,6 +316,12 @@ public class SearchShortestQueryHandler : tag = path.Select(e => e.Tag).Last(); nextTag = path.Select(e => e.Tag).Last(); + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { Uuid = source.Guid, @@ -312,12 +335,14 @@ public class SearchShortestQueryHandler : CityUuid = source.City.Guid, CityName = source.City.Name, TimeToNextAddress = tag.TimeToNextAddress, - CostToNextAddress = tag.CostToNextAddress, + CostToNextAddress = costToNextAddress, CurrentAddressStopTime = tag.CurrentAddressStopTime, Order = addressOrder, RouteAddressUuid = tag.RouteAddress.Guid }); + addressOrder++; + lastRouteAddressGuid = vehicleEnrollments .Single(e => e.Id == tag.VehicleEnrollmentId) .RouteAddressDetails @@ -328,7 +353,6 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Guid; - addressOrder++; addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { Uuid = target.Guid, @@ -358,13 +382,29 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Id; + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + vehicleEnrollmentDtos.Add( new VehicleEnrollmentSearchVehicleEnrollmentDto() { DepartureTime = tag.VehicleEnrollment - .GetDepartureTime(firstRouteAddressId), + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), ArrivalTime = tag.VehicleEnrollment - .GetArrivalTime(lastRouteAddressId), + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = tag.VehicleEnrollment .GetTravelTime(firstRouteAddressId, lastRouteAddressId), @@ -377,10 +417,8 @@ public class SearchShortestQueryHandler : NumberOfStops = tag.VehicleEnrollment .GetNumberOfStops(firstRouteAddressId, lastRouteAddressId), - Currency = tag.VehicleEnrollment.Currency.Name, - Cost = tag.VehicleEnrollment - .GetCost(firstRouteAddressId, - lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, @@ -393,28 +431,6 @@ public class SearchShortestQueryHandler : } - foreach (var vehicleEnrollmentDto in vehicleEnrollmentDtos) - { - foreach (var addressDto in vehicleEnrollmentDto.Addresses) - { - addressDto.CostToNextAddress = await _currencyConverterService - .ConvertAsync(addressDto.CostToNextAddress, - vehicleEnrollments - .First(e => e.Guid == vehicleEnrollmentDto.Uuid) - .Currency, - _sessionCurrencyService.Currency, cancellationToken); - } - - vehicleEnrollmentDto.Currency = vehicleEnrollmentDto.Currency; - vehicleEnrollmentDto.Cost = vehicleEnrollmentDto.Addresses - .Aggregate((decimal)0, - (sum, next) => sum += next.CostToNextAddress); - } - - var cost = vehicleEnrollmentDtos - .Aggregate((decimal)0, - (sum, next) => sum += next.Cost); - var departureTime = vehicleEnrollmentDtos .OrderBy(e => e.Order).First().DepartureTime; var arrivalTime = vehicleEnrollmentDtos @@ -422,14 +438,20 @@ public class SearchShortestQueryHandler : var timeInStops = vehicleEnrollmentDtos .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); return new VehicleEnrollmentSearchDto() { - DepartureTime = departureTime, - ArrivalTime = arrivalTime, + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = arrivalTime - departureTime, TimeInStops = timeInStops, NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, Enrollments = vehicleEnrollmentDtos }; } diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs index 2409797..e8fc53b 100644 --- a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs @@ -12,6 +12,10 @@ public sealed class VehicleEnrollmentSearchDto public int NumberOfTransfers { get; set; } + public string Currency { get; set; } + + public decimal Cost { get; set; } + public ICollection Enrollments { get; set; } } diff --git a/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs new file mode 100644 index 0000000..d072655 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs @@ -0,0 +1,32 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +public sealed class SearchAllViewModel +{ + public Guid DepartureAddressUuid { get; set; } + + public Guid ArrivalAddressUuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs index e10020e..5ed284a 100644 --- a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs +++ b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.ViewModels; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch .Queries.SearchShortest; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch + .Queries.SearchAll; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -11,8 +14,8 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; [Route("vehicleEnrollmentSearch")] public class VehicleEnrollmentSearchController : ControllerBase { - [HttpGet] - [SwaggerOperation("Search vehicle enrollments with transfers")] + [HttpGet("shortest")] + [SwaggerOperation("Search shortest vehicle enrollments with transfers")] [SwaggerResponse( StatusCodes.Status200OK, "Search successful", typeof(VehicleEnrollmentSearchDto))] @@ -32,7 +35,7 @@ public class VehicleEnrollmentSearchController : ControllerBase [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] - public async Task> Add( + public async Task> SearchShortest( [FromQuery] SearchShortestViewModel viewModel, CancellationToken cancellationToken) { @@ -51,4 +54,66 @@ public class VehicleEnrollmentSearchController : ControllerBase }, cancellationToken)); } + + [HttpGet("all")] + [SwaggerOperation("Search all vehicle enrollments with transfers")] + [SwaggerResponse( + StatusCodes.Status200OK, "Search successful", + typeof(IEnumerable))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "No enrollments found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task>> + SearchAll( + [FromQuery] SearchAllViewModel viewModel, + [FromQuery] SortQuery sortQuery, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new SearchAllQuery() + { + DepartureAddressGuid = viewModel.DepartureAddressUuid, + ArrivalAddressGuid = viewModel.ArrivalAddressUuid, + DepartureDate = viewModel.DepartureDate, + VehicleTypes = viewModel.VehicleTypes + .Select(e => VehicleType.FromName(e)).ToHashSet(), + Sort = sortQuery.Sort, + TravelTimeGreaterThanOrEqualTo = + viewModel.TravelTimeGreaterThanOrEqualTo, + TravelTimeLessThanOrEqualTo = + viewModel.TravelTimeLessThanOrEqualTo, + CostGreaterThanOrEqualTo = + viewModel.CostGreaterThanOrEqualTo, + CostLessThanOrEqualTo = + viewModel.CostLessThanOrEqualTo, + NumberOfTransfersGreaterThanOrEqualTo = + viewModel.NumberOfTransfersGreaterThanOrEqualTo, + NumberOfTransfersLessThanOrEqualTo = + viewModel.NumberOfTransfersLessThanOrEqualTo, + DepartureTimeGreaterThanOrEqualTo = + viewModel.DepartureTimeGreaterThanOrEqualTo, + DepartureTimeLessThanOrEqualTo = + viewModel.DepartureTimeLessThanOrEqualTo, + ArrivalTimeGreaterThanOrEqualTo = + viewModel.ArrivalTimeGreaterThanOrEqualTo, + ArrivalTimeLessThanOrEqualTo = + viewModel.ArrivalTimeLessThanOrEqualTo + }, + cancellationToken)); + } } From afe626bd78f34d73ba0800bfe907d505279e9eda Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Mon, 26 May 2025 12:16:46 +0300 Subject: [PATCH 19/35] add LiqPay integration for ticket purchase hosted services deletes ticket groups with reserved status that were created more than 10 minutes ago payment link expires in 10 minutes from the time it was created --- src/Application/Application.csproj | 1 + .../Authorization/AllowAllRequirement.cs | 17 + .../MustBeAuthenticatedRequirement.cs | 3 - .../Services/LiqPayPaymentService.cs | 13 + .../GetPaymentLink/GetPaymentLinkCommand.cs | 25 + .../GetPaymentLinkCommandAuthorizer.cs | 32 + .../GetPaymentLinkCommandHandler.cs | 474 ++++++++ .../GetPaymentLinkCommandValidator.cs | 101 ++ .../ProcessCallback/ProcessCallbackCommand.cs | 11 + .../ProcessCallbackCommandAuthorizer.cs | 14 + .../ProcessCallbackCommandHandler.cs | 70 ++ .../ProcessCallbackCommandValidator.cs | 23 + .../Models/TicketGroupPaymentTicketModel.cs | 14 + .../ViewModels/CallbackViewModel.cs | 9 + .../ViewModels/TicketGroupPaymentViewModel.cs | 21 + .../ViewModels/TicketPaymentViewModel.cs | 14 + src/Application/Payments/PaymentLinkDto.cs | 6 + .../Resources/Localization/en-US.json | 3 + .../AddTicketGroup/AddTicketGroupCommand.cs | 2 +- .../AddTicketGroupCommandHandler.cs | 10 +- .../AddTicketGroupCommandValidator.cs | 9 + .../RemoveOldReservedTicketGroupsCommand.cs | 8 + ...ldReservedTicketGroupsCommandAuthorizer.cs | 14 + ...veOldReservedTicketGroupsCommandHandler.cs | 44 + ...OldReservedTicketGroupsCommandValidator.cs | 20 + .../ViewModels/AddTicketGroupViewModel.cs | 2 +- src/Application/packages.lock.json | 6 + .../Configuration/Configuration.cs | 6 + .../Infrastructure/Configuration.cs | 6 +- src/Configuration/packages.lock.json | 1 + src/Domain/Entities/TicketGroup.cs | 2 +- src/Domain/Enums/TicketStatus.cs | 25 + src/HttpApi/Controllers/PaymentController.cs | 92 ++ .../Controllers/TicketGroupsController.cs | 8 +- .../ReservedTicketRemoverHostedService.cs | 41 + src/HttpApi/Program.cs | 3 + src/HttpApi/appsettings.Development.json | 10 + src/HttpApi/appsettings.json | 10 + src/HttpApi/packages.lock.json | 1 + src/Identity/packages.lock.json | 6 + src/Infrastructure/ConfigurationOptions.cs | 20 +- .../ExchangeApiCurrencyConverterService.cs | 2 + .../Services/LiqPayPaymentService.cs | 78 ++ src/Infrastructure/packages.lock.json | 1 + src/Persistence/InMemory/InMemoryDbContext.cs | 5 + .../TicketGroupConfiguration.cs | 38 +- ...743_Add_status_to_Ticket_Group.Designer.cs | 1019 +++++++++++++++++ ...250524184743_Add_status_to_Ticket_Group.cs | 55 + .../PostgreSqlDbContextModelSnapshot.cs | 9 +- .../PostgreSql/PostgreSqlDbContext.cs | 5 + .../TypeConverters/TicketStatusConverter.cs | 13 + src/Persistence/packages.lock.json | 6 + .../packages.lock.json | 1 + 53 files changed, 2396 insertions(+), 33 deletions(-) create mode 100644 src/Application/Common/Authorization/AllowAllRequirement.cs create mode 100644 src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs create mode 100644 src/Application/Payments/PaymentLinkDto.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs create mode 100644 src/Domain/Enums/TicketStatus.cs create mode 100644 src/HttpApi/Controllers/PaymentController.cs create mode 100644 src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs create mode 100644 src/Infrastructure/Services/LiqPayPaymentService.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs create mode 100644 src/Persistence/TypeConverters/TicketStatusConverter.cs diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 9b7a24d..0e77ba6 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Application/Common/Authorization/AllowAllRequirement.cs b/src/Application/Common/Authorization/AllowAllRequirement.cs new file mode 100644 index 0000000..05c7d78 --- /dev/null +++ b/src/Application/Common/Authorization/AllowAllRequirement.cs @@ -0,0 +1,17 @@ +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class AllowAllRequirement : IAuthorizationRequirement +{ + class MustBeAuthenticatedRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + AllowAllRequirement request, + CancellationToken cancellationToken) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + } +} diff --git a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs index 809c638..685e587 100644 --- a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs +++ b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs @@ -1,4 +1,3 @@ -// using cuqmbr.TravelGuide.Application.Common.Exceptions; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Common.Authorization; @@ -17,8 +16,6 @@ public class MustBeAuthenticatedRequirement : IAuthorizationRequirement if (!request.IsAuthenticated) { return Task.FromResult(AuthorizationResult.Fail()); - // TODO: Remove UnAuthorizedException, isn't used - // throw new UnAuthorizedException(); } return Task.FromResult(AuthorizationResult.Succeed()); diff --git a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..4a0551f --- /dev/null +++ b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +public interface LiqPayPaymentService +{ + Task GetPaymentLinkAsync( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath); + + Task IsValidSignatureAsync(string postData, string postSignature); +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs new file mode 100644 index 0000000..98f3956 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Payments.LiqPay.TicketGroups.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public record GetPaymentLinkCommand : IRequest +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + + public ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs new file mode 100644 index 0000000..a1ad297 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetPaymentLinkCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetPaymentLinkCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs new file mode 100644 index 0000000..28c9328 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -0,0 +1,474 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + private readonly CurrencyConverterService _currencyConverterService; + + private readonly LiqPayPaymentService _liqPayPaymentService; + + private readonly IStringLocalizer _localizer; + + public GetPaymentLinkCommandHandler( + UnitOfWork unitOfWork, + CurrencyConverterService currencyConverterService, + LiqPayPaymentService liqPayPaymentService, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _currencyConverterService = currencyConverterService; + _liqPayPaymentService = liqPayPaymentService; + _localizer = localizer; + } + + public async Task Handle( + GetPaymentLinkCommand request, + CancellationToken cancellationToken) + { + // Check whether provided vehicle enrollments are present in datastore. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + if (vehicleEnrollmentGuids.Count() > vehicleEnrollments.Count) + { + throw new NotFoundException(); + } + } + + + // Check whether provided arrival and departure address guids + // are used in provided vehicle enrollment and + // and are in the correct order. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + foreach (var t in request.Tickets) + { + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + if (departureRouteAddress.RouteId != ve.RouteId || + arrivalRouteAddress.RouteId != ve.RouteId) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + + if (departureRouteAddress.Order > arrivalRouteAddress.Order) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Check availability of free places. + { + // Get all tickets for vehicle enrollments requested in ticket group. + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + + var ticketGroupTickets = (await _unitOfWork.TicketRepository + .GetPageAsync( + e => + vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && + unavailableTicketStatuses.Contains(e.TicketGroup.Status), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Get all vehicle enrollments requested in ticket group + // together with vehicles. + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + e => e.Vehicle, + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + // Get all route addresses of vehicle enrollments + // requested in ticket group. + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeIds.Contains(e.RouteId), + 1, int.MaxValue, cancellationToken)) + .Items; + + // For each ticket in request. + foreach (var requestTicket in request.Tickets) + { + // Get vehicle enrollment of requested ticket. + var requestVehicleEnrollment = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid); + + // Get bought tickets of vehicle enrollment of requested ticket. + var tickets = ticketGroupTickets.Where(t => + t.VehicleEnrollmentId == requestVehicleEnrollment.Id); + + // Get route addresses of vehicle enrollment. + var ticketRouteAddresses = routeAddresses + .Where(e => e.RouteId == requestVehicleEnrollment.RouteId) + .OrderBy(e => e.Order); + + + // Count available capacity. + + // Get total capacity in requested vehicle. + int totalCapacity; + var vehicle = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid) + .Vehicle; + if (vehicle.VehicleType.Equals(VehicleType.Bus)) + { + totalCapacity = ((Bus)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Aircraft)) + { + totalCapacity = ((Aircraft)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Train)) + { + totalCapacity = ((Train)vehicle).Capacity; + } + else + { + throw new NotImplementedException(); + } + + int takenCapacity = 0; + + // For each bought ticket. + foreach (var ticket in tickets) + { + // Get departure and arrival route address + // of requested ticket. + var requestDepartureRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.DepartureRouteAddressGuid); + var requestArrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.ArrivalRouteAddressGuid); + + // Get departure and arrival route address + // of bought ticket. + var departureRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.DepartureRouteAddressId); + var arrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.ArrivalRouteAddressId); + + + // Count taken capacity in requested vehicle + // accounting for requested ticket + // departure and arrival route addresses. + // The algorithm is the same as vehicle enrollment + // time overlap check. + if ((requestDepartureRouteAddress.Order >= + departureRouteAddress.Order && + requestDepartureRouteAddress.Order < + arrivalRouteAddress.Order) || + (requestArrivalRouteAddress.Order <= + arrivalRouteAddress.Order && + requestArrivalRouteAddress.Order > + departureRouteAddress.Order) || + (requestDepartureRouteAddress.Order <= + departureRouteAddress.Order && + requestArrivalRouteAddress.Order >= + arrivalRouteAddress.Order)) + { + takenCapacity++; + } + } + + var availableCapacity = totalCapacity - takenCapacity; + + if (availableCapacity <= 0) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Calculate travel time and cost. + + var ticketsDetails = new List<(short order, DateTimeOffset departureTime, + DateTimeOffset arrivalTime, decimal cost, Currency currency)>(); + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id); + + var allRouteAddressDetails = (await _unitOfWork + .RouteAddressDetailRepository.GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress, + 1, int.MaxValue, cancellationToken)) + .Items; + + + foreach (var t in request.Tickets.OrderBy(t => t.Order)) + { + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + var departureRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid) + .Id; + var arrivalRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid) + .Id; + + var verad = allRouteAddressDetails + .Where(arad => arad.VehicleEnrollmentId == ve.Id) + .OrderBy(rad => rad.RouteAddress.Order) + .TakeWhile(rad => rad.Id != arrivalRouteAddressId); + + + // TODO: This counts departure address stop time which is + // not wrong but may be not desired. + var timeToDeparture = verad + .TakeWhile(rad => rad.Id != departureRouteAddressId) + .Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var departureTime = ve.DepartureTime.Add(timeToDeparture); + + + var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var arrivalTime = ve.DepartureTime.Add(timeToArrival); + + + var costToDeparture = verad + .TakeWhile(rad => rad.Id != departureRouteAddressId) + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var costToArrival = verad + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var cost = costToArrival - costToDeparture; + + + ticketsDetails.Add( + (t.Order, departureTime, arrivalTime, cost, ve.Currency)); + } + } + + // Check whether there are overlaps in ticket departure/arrival times. + { + for (int i = 1; i < ticketsDetails.Count; i++) + { + var previousTd = ticketsDetails[i - 1]; + var currentTd = ticketsDetails[i]; + + if (previousTd.arrivalTime >= currentTd.departureTime) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Create entity and insert into a datastore. + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeAddressGuids.Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var travelTime = + ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - + ticketsDetails.OrderBy(td => td.order).First().departureTime; + + var entity = new TicketGroup() + { + PassangerFirstName = request.PassangerFirstName, + PassangerLastName = request.PassangerLastName, + PassangerPatronymic = request.PassangerPatronymic, + PassangerSex = request.PassangerSex, + PassangerBirthDate = request.PassangerBirthDate, + PurchaseTime = DateTimeOffset.UtcNow, + Status = TicketStatus.Reserved, + TravelTime = travelTime, + Tickets = request.Tickets.Select( + t => + { + var ve = vehicleEnrollments.First( + ve => ve.Guid == t.VehicleEnrollmentGuid); + + + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + + var detail = ticketsDetails + .SingleOrDefault(td => td.order == t.Order); + + var currency = Currency.UAH; + var cost = _currencyConverterService + .ConvertAsync( + detail.cost, detail.currency, currency, + cancellationToken).Result; + + return new Ticket() + { + DepartureRouteAddressId = departureRouteAddress.Id, + DepartureRouteAddress = departureRouteAddress, + ArrivalRouteAddressId = arrivalRouteAddress.Id, + ArrivalRouteAddress = arrivalRouteAddress, + Order = t.Order, + Cost = cost, + Currency = currency, + VehicleEnrollmentId = ve.Id + }; + }) + .ToArray() + }; + + entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + + var amount = entity.Tickets.Sum(e => e.Cost); + var guid = entity.Guid; + var validity = TimeSpan.FromMinutes(10); + var resultPath = request.ResultPath; + var callbackPath = "/payments/liqPay/ticket/callback"; + + var paymentLink = await _liqPayPaymentService + .GetPaymentLinkAsync( + amount, Currency.UAH, guid.ToString(), validity, + _localizer["PaymentProcessing.TicketPaymentDescription"], + resultPath, callbackPath); + + return new PaymentLinkDto() { PaymentLink = paymentLink }; + } + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs new file mode 100644 index 0000000..7fbc8eb --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs @@ -0,0 +1,101 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandValidator : + AbstractValidator +{ + public GetPaymentLinkCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(tg => tg.PassangerFirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerLastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerPatronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerSex) + .Must((tg, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(tg => tg.PassangerBirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.VehicleEnrollmentGuid) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.Order) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(tg => tg.Tickets).ChildRules(t => + { + t.RuleFor(t => t.DepartureRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.ArrivalRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.Order) + .GreaterThanOrEqualTo(short.MinValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + short.MinValue)) + .LessThanOrEqualTo(short.MaxValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + short.MaxValue)); + + t.RuleFor(t => t.VehicleEnrollmentGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + }); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs new file mode 100644 index 0000000..60b6068 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public record ProcessCallbackCommand : IRequest +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs new file mode 100644 index 0000000..200d9db --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(ProcessCallbackCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs new file mode 100644 index 0000000..099fc63 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -0,0 +1,70 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using System.Text; +using Newtonsoft.Json; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly LiqPayPaymentService _liqPayPaymentService; + + public ProcessCallbackCommandHandler( + UnitOfWork unitOfWork, + LiqPayPaymentService liqPayPaymentService) + { + _unitOfWork = unitOfWork; + _liqPayPaymentService = liqPayPaymentService; + } + + public async Task Handle( + ProcessCallbackCommand request, + CancellationToken cancellationToken) + { + var isSignatureValid = await _liqPayPaymentService + .IsValidSignatureAsync(request.Data, request.Signature); + + if (!isSignatureValid) + { + throw new ForbiddenException(); + } + + var dataBytes = Convert.FromBase64String(request.Data); + var dataJson = Encoding.UTF8.GetString(dataBytes); + + var data = JsonConvert.DeserializeObject(dataJson); + + string status = data.status; + + var ticketGroupGuid = Guid.Parse((string)data.order_id); + var ticketGroup = await _unitOfWork.TicketGroupRepository + .GetOneAsync(e => e.Guid == ticketGroupGuid, cancellationToken); + + if (ticketGroup == null || + ticketGroup.Status == TicketStatus.Purchased) + { + throw new ForbiddenException(); + } + + if (status.Equals("error") || status.Equals("failure")) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(ticketGroup, cancellationToken); + } + else if (status.Equals("success")) + { + ticketGroup.Status = TicketStatus.Purchased; + await _unitOfWork.TicketGroupRepository + .UpdateOneAsync(ticketGroup, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs new file mode 100644 index 0000000..70faa59 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandValidator : + AbstractValidator +{ + public ProcessCallbackCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Data) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Signature) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs new file mode 100644 index 0000000..4bc5a3b --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; + +public sealed class TicketGroupPaymentTicketModel +{ + public Guid DepartureRouteAddressGuid { get; set; } + + public Guid ArrivalRouteAddressGuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentGuid { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs new file mode 100644 index 0000000..ca696c0 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs @@ -0,0 +1,9 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class CallbackViewModel +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs new file mode 100644 index 0000000..4d6d83b --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketGroupPaymentViewModel +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + + public ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs new file mode 100644 index 0000000..f30684f --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketPaymentViewModel +{ + public Guid DepartureRouteAddressUuid { get; set; } + + public Guid ArrivalRouteAddressUuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentUuid { get; set; } +} diff --git a/src/Application/Payments/PaymentLinkDto.cs b/src/Application/Payments/PaymentLinkDto.cs new file mode 100644 index 0000000..d47bde9 --- /dev/null +++ b/src/Application/Payments/PaymentLinkDto.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Payments; + +public sealed class PaymentLinkDto +{ + public string PaymentLink { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index be9ac72..dbec72c 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -58,5 +58,8 @@ "Title": "One or more internal server errors occurred.", "Detail": "Report this error to service's support team." } + }, + "PaymentProcessing": { + "TicketPaymentDescription": "Ticket purchase." } } diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs index 554344b..a77bfee 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs @@ -18,7 +18,7 @@ public record AddTicketGroupCommand : IRequest public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public TicketStatus Status { get; set; } public ICollection Tickets { get; set; } diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs index efe1455..25d4092 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -128,11 +128,17 @@ public class AddTicketGroupCommandHandler : var vehicleEnrollmentGuids = request.Tickets.Select(t => t.VehicleEnrollmentGuid); + var unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + var ticketGroupTickets = (await _unitOfWork.TicketRepository .GetPageAsync( e => vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && - e.TicketGroup.Returned == false, + unavailableTicketStatuses.Contains(e.TicketGroup.Status), 1, int.MaxValue, cancellationToken)) .Items; @@ -431,7 +437,7 @@ public class AddTicketGroupCommandHandler : PassangerSex = request.PassangerSex, PassangerBirthDate = request.PassangerBirthDate, PurchaseTime = request.PurchaseTime, - Returned = request.Returned, + Status = request.Status, TravelTime = travelTime, Tickets = request.Tickets.Select( t => diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs index 8ead4b4..58a8029 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -67,6 +67,15 @@ public class AddTicketGroupCommandValidator : AbstractValidator tg.Status) + .Must((tg, s) => TicketStatus.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + TicketStatus.Enumerations.Values.Select(e => e.Name)))); + RuleFor(tg => tg.Tickets) .IsUnique(t => t.VehicleEnrollmentGuid) .WithMessage(localizer["FluentValidation.IsUnique"]); diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs new file mode 100644 index 0000000..9284fd0 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public record RemoveOldReservedTicketGroupsCommand : IRequest +{ + public TimeSpan ReservedFor { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs new file mode 100644 index 0000000..b73e99f --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy( + RemoveOldReservedTicketGroupsCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs new file mode 100644 index 0000000..37373af --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public RemoveOldReservedTicketGroupsCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + RemoveOldReservedTicketGroupsCommand request, + CancellationToken cancellationToken) + { + var statusesToRemove = new TicketStatus[] + { + TicketStatus.Reserved + }; + + var entities = (await _unitOfWork.TicketGroupRepository + .GetPageAsync( + e => + statusesToRemove.Contains(e.Status) && + DateTimeOffset.UtcNow - e.PurchaseTime > request.ReservedFor, + 1, int.MaxValue, cancellationToken)) + .Items; + + foreach (var entity in entities) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(entity, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs new file mode 100644 index 0000000..71a17d2 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandValidator : + AbstractValidator +{ + public RemoveOldReservedTicketGroupsCommandValidator( + IStringLocalizer localizer) + { + RuleFor(v => v.ReservedFor) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs index dd292ff..9cac564 100644 --- a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs +++ b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs @@ -14,7 +14,7 @@ public sealed class AddTicketGroupViewModel public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public string Status { get; set; } public ICollection Tickets { get; set; } diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index ac19a92..14b60f0 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -59,6 +59,12 @@ "Microsoft.Extensions.Options": "9.0.4" } }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "QuikGraph": { "type": "Direct", "requested": "[2.5.0, )", diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index 0c6eaa9..5a8d825 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -4,6 +4,8 @@ using PersistenceConfigurationOptions = cuqmbr.TravelGuide.Persistence.ConfigurationOptions; using ApplicationConfigurationOptions = cuqmbr.TravelGuide.Application.ConfigurationOptions; +using InfrastructureConfigurationOptions = + cuqmbr.TravelGuide.Infrastructure.ConfigurationOptions; using IdentityConfigurationOptions = cuqmbr.TravelGuide.Identity.ConfigurationOptions; @@ -33,6 +35,10 @@ public static class Configuration configuration.GetSection( ApplicationConfigurationOptions.SectionName)); + services.AddOptions().Bind( + configuration.GetSection( + InfrastructureConfigurationOptions.SectionName)); + services.AddOptions().Bind( configuration.GetSection( IdentityConfigurationOptions.SectionName)); diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index 7e8793c..3df6bb0 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Infrastructure.Services; using Microsoft.Extensions.DependencyInjection; namespace cuqmbr.TravelGuide.Configuration.Infrastructure; @@ -14,7 +15,10 @@ public static class Configuration services .AddScoped< CurrencyConverterService, - ExchangeApiCurrencyConverterService>(); + ExchangeApiCurrencyConverterService>() + .AddScoped< + cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService, + cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>(); return services; } diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index 8fb4534..f253c37 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -843,6 +843,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs index b044ceb..c40a2ae 100644 --- a/src/Domain/Entities/TicketGroup.cs +++ b/src/Domain/Entities/TicketGroup.cs @@ -16,7 +16,7 @@ public sealed class TicketGroup : EntityBase public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public TicketStatus Status { get; set; } public TimeSpan TravelTime { get; set; } diff --git a/src/Domain/Enums/TicketStatus.cs b/src/Domain/Enums/TicketStatus.cs new file mode 100644 index 0000000..b3c0b3a --- /dev/null +++ b/src/Domain/Enums/TicketStatus.cs @@ -0,0 +1,25 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +public abstract class TicketStatus : Enumeration +{ + public static readonly TicketStatus Reserved = new ReservedTicketStatus(); + public static readonly TicketStatus Returned = new ReturnedTicketStatus(); + public static readonly TicketStatus Purchased = new PurchasedTicketStatus(); + + protected TicketStatus(int value, string name) : base(value, name) { } + + private sealed class ReservedTicketStatus : TicketStatus + { + public ReservedTicketStatus() : base(0, "reserved") { } + } + + private sealed class ReturnedTicketStatus : TicketStatus + { + public ReturnedTicketStatus() : base(1, "returned") { } + } + + private sealed class PurchasedTicketStatus : TicketStatus + { + public PurchasedTicketStatus() : base(2, "purchased") { } + } +} diff --git a/src/HttpApi/Controllers/PaymentController.cs b/src/HttpApi/Controllers/PaymentController.cs new file mode 100644 index 0000000..b7c2e70 --- /dev/null +++ b/src/HttpApi/Controllers/PaymentController.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Payments; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("payments")] +public class PaymentController : ControllerBase +{ + [HttpPost("liqPay/ticket/getLink")] + [SwaggerOperation("Get payment link for provided ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly created", + typeof(PaymentLinkDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> LiqPayTicketGetLink( + [FromBody] TicketGroupPaymentViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status200OK, + await Mediator.Send( + new GetPaymentLinkCommand() + { + PassangerFirstName = viewModel.PassangerFirstName, + PassangerLastName = viewModel.PassangerLastName, + PassangerPatronymic = viewModel.PassangerPatronymic, + PassangerSex = Sex.FromName(viewModel.PassangerSex), + PassangerBirthDate = viewModel.PassangerBirthDate, + Tickets = viewModel.Tickets.Select(e => + new TicketGroupPaymentTicketModel() + { + DepartureRouteAddressGuid = e.DepartureRouteAddressUuid, + ArrivalRouteAddressGuid = e.ArrivalRouteAddressUuid, + Order = e.Order, + VehicleEnrollmentGuid = e.VehicleEnrollmentUuid + }) + .ToArray(), + ResultPath = viewModel.ResultPath + }, + cancellationToken)); + } + + [Consumes("application/x-www-form-urlencoded")] + [HttpPost("liqPay/ticket/callback")] + [SwaggerOperation("Process LiqPay callback for ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly processed")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task LiqPayTicketCallback( + [FromForm] CallbackViewModel viewModel, + CancellationToken cancellationToken) + { + await Mediator.Send( + new ProcessCallbackCommand() + { + Data = viewModel.Data, + Signature = viewModel.Signature + }, + cancellationToken); + } +} diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs index 59b8cb6..adf8e3d 100644 --- a/src/HttpApi/Controllers/TicketGroupsController.cs +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -1,14 +1,8 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -using cuqmbr.TravelGuide.Application.Common.Models; -using cuqmbr.TravelGuide.Application.Common.ViewModels; using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.TicketGroups; using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; -// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; -// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; -// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.UpdateTicketGroup; -// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.DeleteTicketGroup; using cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; using cuqmbr.TravelGuide.Application.TicketGroups.Models; @@ -56,7 +50,7 @@ public class TicketGroupsController : ControllerBase PassangerSex = Sex.FromName(viewModel.PassangerSex), PassangerBirthDate = viewModel.PassangerBirthDate, PurchaseTime = viewModel.PurchaseTime, - Returned = viewModel.Returned, + Status = TicketStatus.FromName(viewModel.Status), Tickets = viewModel.Tickets.Select(e => new TicketModel() { diff --git a/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs new file mode 100644 index 0000000..93de31a --- /dev/null +++ b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs @@ -0,0 +1,41 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +namespace cuqmbr.TravelGuide.HttpApi.HostedServices; + +public class ReservedTicketRemoverHostedService : BackgroundService +{ + private Timer _timer = null; + + public ReservedTicketRemoverHostedService( + IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider; + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _timer = new Timer(async (state) => + { + using var scope = ServiceProvider.CreateScope(); + var _mediator = scope.ServiceProvider + .GetRequiredService(); + + await _mediator.Send( + new RemoveOldReservedTicketGroupsCommand() + { + ReservedFor = TimeSpan.FromMinutes(10) + }, cancellationToken); + }, + null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Dispose(); + await base.StopAsync(cancellationToken); + } +} diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index 472d41e..015f880 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -6,6 +6,7 @@ using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.HttpApi.Services; +using cuqmbr.TravelGuide.HttpApi.HostedServices; using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; using System.Net; @@ -33,6 +34,8 @@ services.AddScoped(); services.AddScoped(); services.AddScoped(); +services.AddHostedService(); + services.AddControllers(); diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 2bdff62..8f2afcb 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -13,6 +13,16 @@ "Localization": { "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" + }, + "Infrastructure": { + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } } }, "Identity": { diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 2bdff62..8f2afcb 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -13,6 +13,16 @@ "Localization": { "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" + }, + "Infrastructure": { + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } } }, "Identity": { diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index 58c6c3e..2d89b37 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -1084,6 +1084,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json index 22ffc53..5c6200a 100644 --- a/src/Identity/packages.lock.json +++ b/src/Identity/packages.lock.json @@ -520,6 +520,11 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -594,6 +599,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index 1d4b8a4..338297e 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -2,5 +2,23 @@ namespace cuqmbr.TravelGuide.Infrastructure; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Infrastructure"; + public static string SectionName { get; } = "Application:Infrastructure"; + + public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } +} + +public sealed class PaymentProcessingConfigurationOptions +{ + public string CallbackAddressBase { get; set; } + + public string ResultAddressBase { get; set; } + + public LiqPayConfigurationOptions LiqPay { get; set; } +} + +public sealed class LiqPayConfigurationOptions +{ + public string PublicKey { get; set; } + + public string PrivateKey { get; set; } } diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs index ee4d9d0..e2bd323 100644 --- a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -3,6 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Domain.Enums; using Newtonsoft.Json; +namespace cuqmbr.TravelGuide.Infrastructure.Services; + // https://github.com/fawazahmed0/exchange-api public sealed class ExchangeApiCurrencyConverterService : diff --git a/src/Infrastructure/Services/LiqPayPaymentService.cs b/src/Infrastructure/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..452305f --- /dev/null +++ b/src/Infrastructure/Services/LiqPayPaymentService.cs @@ -0,0 +1,78 @@ +using System.Dynamic; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class LiqPayPaymentService : + cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService +{ + private readonly LiqPayConfigurationOptions _configuration; + private readonly string _callbackAddressBase; + private readonly string _resultAddressBase; + + private readonly IHttpClientFactory _httpClientFactory; + + public LiqPayPaymentService( + IOptions configurationOptions, + IHttpClientFactory httpClientFactory) + { + _configuration = configurationOptions.Value.PaymentProcessing.LiqPay; + _callbackAddressBase = + configurationOptions.Value.PaymentProcessing.CallbackAddressBase; + _resultAddressBase = + configurationOptions.Value.PaymentProcessing.ResultAddressBase; + } + + public Task GetPaymentLinkAsync( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath) + { + dynamic request = new ExpandoObject(); + + request.version = 3; + request.public_key = _configuration.PublicKey; + request.action = "pay"; + request.amount = amount; + request.currency = currency.Name.ToUpper(); + request.description = description; + request.order_id = orderId; + request.expired_date = DateTimeOffset.UtcNow.Add(validity) + .ToString("yyyy-MM-dd HH:mm:ss"); + request.result_url = $"{_resultAddressBase}{resultPath}"; + request.server_url = $"{_callbackAddressBase}{callbackPath}"; + + var requestJsonString = (string)JsonConvert.SerializeObject(request); + + + var requestJsonStringBytes = Encoding.UTF8.GetBytes(requestJsonString); + + var data = Convert.ToBase64String(requestJsonStringBytes); + + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + data + + _configuration.PrivateKey))); + + + return Task.FromResult( + "https://www.liqpay.ua/api/3/checkout" + + $"?data={data}&signature={signature}"); + } + + public Task IsValidSignatureAsync(string postData, string postSignature) + { + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + postData + + _configuration.PrivateKey))); + + return Task.FromResult(postSignature.Equals(signature)); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index f67a272..b07e4dd 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -254,6 +254,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 3dc85f8..41f3171 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -47,6 +47,11 @@ public class InMemoryDbContext : DbContext .HaveColumnType("varchar(32)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs index 9bc2947..10e4a65 100644 --- a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -14,18 +14,36 @@ public class TicketGroupConfiguration : BaseConfiguration .HasColumnName("passanger_sex") .IsRequired(true); + builder + .Property(tg => tg.Status) + .HasColumnName("status") + .IsRequired(true); + builder .ToTable( "ticket_groups", - tg => tg.HasCheckConstraint( - "ck_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(tg => tg.PassangerSex) + tg => + { + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.PassangerSex) .Metadata.GetColumnName()}", - $"{builder.Property(g => g.PassangerSex) + $"{builder.Property(g => g.PassangerSex) .Metadata.GetColumnName()} IN ('{String - .Join("', '", Sex.Enumerations - .Values.Select(v => v.Name))}')")); + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')"); + + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.Status) + .Metadata.GetColumnName()}", + $"{builder.Property(g => g.Status) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", TicketStatus.Enumerations + .Values.Select(v => v.Name))}')"); + }); base.Configure(builder); @@ -60,12 +78,6 @@ public class TicketGroupConfiguration : BaseConfiguration .HasColumnType("timestamptz") .IsRequired(true); - builder - .Property(a => a.Returned) - .HasColumnName("returned") - .HasColumnType("boolean") - .IsRequired(true); - builder .Property(a => a.TravelTime) .HasColumnName("travel_time") diff --git a/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs new file mode 100644 index 0000000..6338c55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs @@ -0,0 +1,1019 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250524184743_Add_status_to_Ticket_Group")] + partial class Add_status_to_Ticket_Group + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs new file mode 100644 index 0000000..d3036f4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_status_to_Ticket_Group : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "returned", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "status", + schema: "application", + table: "ticket_groups", + type: "varchar(32)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups", + sql: "status IN ('reserved', 'returned', 'purchased')"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropColumn( + name: "status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "returned", + schema: "application", + table: "ticket_groups", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index fb4e67f..e4f59b1 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -577,9 +577,10 @@ namespace Persistence.PostgreSql.Migrations .HasColumnType("timestamptz") .HasColumnName("purchase_time"); - b.Property("Returned") - .HasColumnType("boolean") - .HasColumnName("returned"); + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); b.Property("TravelTime") .HasColumnType("interval") @@ -594,6 +595,8 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("ticket_groups", "application", t => { t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); }); }); diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 11e5d63..0f5571c 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -54,6 +54,11 @@ public class PostgreSqlDbContext : DbContext .HaveColumnType("varchar(32)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/TypeConverters/TicketStatusConverter.cs b/src/Persistence/TypeConverters/TicketStatusConverter.cs new file mode 100644 index 0000000..5220fc8 --- /dev/null +++ b/src/Persistence/TypeConverters/TicketStatusConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class TicketStatusConverter : ValueConverter +{ + public TicketStatusConverter() + : base( + v => v.Name, + v => TicketStatus.FromName(v)) + { } +} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index 3cb01b7..4833b36 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -266,6 +266,11 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -334,6 +339,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 4967b9d..117f2e7 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -987,6 +987,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } From 91805bc9ad03307d57f6f8f511e4e5e90b964db1 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Mon, 26 May 2025 12:36:48 +0300 Subject: [PATCH 20/35] fix: pass cancellationToken to library methods that accept them --- .../Services/LiqPayPaymentService.cs | 4 +-- .../GetPaymentLinkCommandHandler.cs | 4 +-- .../ProcessCallbackCommandHandler.cs | 4 +-- .../Services/LiqPayPaymentService.cs | 10 +++---- .../InMemory/InMemoryUnitOfWork.cs | 2 +- .../Repositories/InMemoryBaseRepository.cs | 24 ++++++++------- .../PostgreSql/PostgreSqlUnitOfWork.cs | 2 +- .../Repositories/PostgreSqlBaseRepository.cs | 30 ++++++++++--------- 8 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs index 4a0551f..768a1c3 100644 --- a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs +++ b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs @@ -4,10 +4,10 @@ namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; public interface LiqPayPaymentService { - Task GetPaymentLinkAsync( + string GetPaymentLink( decimal amount, Currency currency, string orderId, TimeSpan validity, string description, string resultPath, string callbackPath); - Task IsValidSignatureAsync(string postData, string postSignature); + bool IsValidSignature(string postData, string postSignature); } diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs index 28c9328..94fcde6 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -462,8 +462,8 @@ public class GetPaymentLinkCommandHandler : var resultPath = request.ResultPath; var callbackPath = "/payments/liqPay/ticket/callback"; - var paymentLink = await _liqPayPaymentService - .GetPaymentLinkAsync( + var paymentLink = _liqPayPaymentService + .GetPaymentLink( amount, Currency.UAH, guid.ToString(), validity, _localizer["PaymentProcessing.TicketPaymentDescription"], resultPath, callbackPath); diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs index 099fc63..1f3d54b 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -27,8 +27,8 @@ public class ProcessCallbackCommandHandler : ProcessCallbackCommand request, CancellationToken cancellationToken) { - var isSignatureValid = await _liqPayPaymentService - .IsValidSignatureAsync(request.Data, request.Signature); + var isSignatureValid = _liqPayPaymentService + .IsValidSignature(request.Data, request.Signature); if (!isSignatureValid) { diff --git a/src/Infrastructure/Services/LiqPayPaymentService.cs b/src/Infrastructure/Services/LiqPayPaymentService.cs index 452305f..70fc88b 100644 --- a/src/Infrastructure/Services/LiqPayPaymentService.cs +++ b/src/Infrastructure/Services/LiqPayPaymentService.cs @@ -27,7 +27,7 @@ public sealed class LiqPayPaymentService : configurationOptions.Value.PaymentProcessing.ResultAddressBase; } - public Task GetPaymentLinkAsync( + public string GetPaymentLink( decimal amount, Currency currency, string orderId, TimeSpan validity, string description, string resultPath, string callbackPath) @@ -60,12 +60,12 @@ public sealed class LiqPayPaymentService : _configuration.PrivateKey))); - return Task.FromResult( + return "https://www.liqpay.ua/api/3/checkout" + - $"?data={data}&signature={signature}"); + $"?data={data}&signature={signature}"; } - public Task IsValidSignatureAsync(string postData, string postSignature) + public bool IsValidSignature(string postData, string postSignature) { var signature = Convert.ToBase64String(SHA1.HashData( Encoding.UTF8.GetBytes( @@ -73,6 +73,6 @@ public sealed class LiqPayPaymentService : postData + _configuration.PrivateKey))); - return Task.FromResult(postSignature.Equals(signature)); + return postSignature.Equals(signature); } } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 4b257ff..78c3f1a 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -73,7 +73,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public async Task SaveAsync(CancellationToken cancellationToken) { - return await _dbContext.SaveChangesAsync(); + return await _dbContext.SaveChangesAsync(cancellationToken); } public void Dispose() diff --git a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs index cb32f90..8e628a8 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs @@ -28,7 +28,7 @@ public abstract class InMemoryBaseRepository : BaseRepository Expression> predicate, CancellationToken cancellationToken) { - return await _dbSet.SingleOrDefaultAsync(predicate); + return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); } public async Task GetOneAsync( @@ -39,19 +39,19 @@ public abstract class InMemoryBaseRepository : BaseRepository return await _dbSet .Include(includeSelector) - .SingleOrDefaultAsync(predicate); + .SingleOrDefaultAsync(predicate, cancellationToken); } public async Task> GetPageAsync( int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -63,13 +63,13 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) .Include(includeSelector) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -81,13 +81,15 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.Where(predicate).CountAsync(); + var count = await _dbSet + .Where(predicate) + .CountAsync(cancellationToken); var entities = await _dbSet .Where(predicate) .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -100,14 +102,16 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.Where(predicate).CountAsync(); + var count = await _dbSet + .Where(predicate) + .CountAsync(cancellationToken); var entities = await _dbSet .Where(predicate) .Skip((pageNumber - 1) * pageSize).Take(pageSize) .Include(includeSelector) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 9b324a2..6898026 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -73,7 +73,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public async Task SaveAsync(CancellationToken cancellationToken) { - return await _dbContext.SaveChangesAsync(); + return await _dbContext.SaveChangesAsync(cancellationToken); } public void Dispose() diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs index a5c1dae..cc3930a 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs @@ -28,7 +28,7 @@ public abstract class PostgreSqlBaseRepository : BaseRepository> predicate, CancellationToken cancellationToken) { - return await _dbSet.SingleOrDefaultAsync(predicate); + return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); } public async Task GetOneAsync( @@ -36,22 +36,21 @@ public abstract class PostgreSqlBaseRepository : BaseRepository> includeSelector, CancellationToken cancellationToken) { - return - await _dbSet - .Include(includeSelector) - .SingleOrDefaultAsync(predicate); + return await _dbSet + .Include(includeSelector) + .SingleOrDefaultAsync(predicate, cancellationToken); } public async Task> GetPageAsync( int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -63,13 +62,13 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -81,13 +80,15 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -100,14 +101,16 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -129,5 +132,4 @@ public abstract class PostgreSqlBaseRepository : BaseRepository Date: Tue, 27 May 2025 12:58:14 +0300 Subject: [PATCH 21/35] remove unnecessary todos --- .../CitiesTests.cs | 21 ------------------- .../CountriesTests.cs | 12 ----------- .../RegionsTests.cs | 21 ------------------- 3 files changed, 54 deletions(-) diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs index 4a4c68e..5a7b514 100644 --- a/tst/Application.IntegrationTests/CitiesTests.cs +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -264,8 +264,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task AddCity_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -279,8 +277,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task UpdateCity_WithAdminRole_CityUpdated() @@ -480,8 +476,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task UpdateCity_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -495,8 +489,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task DeleteCity_WithAdminRole_CityDeleted() @@ -584,8 +576,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task DeleteCity_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -599,8 +589,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCity_WithAdminRole_CityReturned() @@ -703,8 +691,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetCity_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -718,8 +704,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCitiesPage_WithAdminRole_CitiesPageReturned() @@ -1556,8 +1540,6 @@ public class CitiesTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetCitiesPage_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -1570,7 +1552,4 @@ public class CitiesTests : TestBase new GetCityQuery(), TestContext.Current.CancellationToken)); } - - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) } diff --git a/tst/Application.IntegrationTests/CountriesTests.cs b/tst/Application.IntegrationTests/CountriesTests.cs index 20e6233..43b5f88 100644 --- a/tst/Application.IntegrationTests/CountriesTests.cs +++ b/tst/Application.IntegrationTests/CountriesTests.cs @@ -248,8 +248,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task UpdateCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -265,8 +263,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task DeleteCountry_WithAdminRole_CountryDeleted() @@ -341,8 +337,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task DeleteCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -357,8 +351,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCountry_WithAdminRole_CountryReturned() @@ -434,8 +426,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -450,8 +440,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCountriesPage_WithAdminRole_CountriesPageReturned() diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 78e0991..7dbc6e8 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -216,8 +216,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task AddRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -231,8 +229,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task UpdateRegion_WithAdminRole_RegionUpdated() @@ -408,8 +404,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -423,8 +417,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task DeleteRegion_WithAdminRole_RegionDeleted() @@ -505,8 +497,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -520,8 +510,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetRegion_WithAdminRole_RegionReturned() @@ -610,8 +598,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -625,8 +611,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() @@ -1141,8 +1125,6 @@ public class RegionsTests : TestBase TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetRegionsPage_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -1155,7 +1137,4 @@ public class RegionsTests : TestBase new GetRegionQuery(), TestContext.Current.CancellationToken)); } - - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) } From 57264b384c7c7da813d453e4206947c509dc2b84 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 27 May 2025 14:08:40 +0300 Subject: [PATCH 22/35] add vehicle and company info to vehicle enrollment search dto --- .../SearchAll/SearchAllQueryHandler.cs | 39 +++++++- .../SearchShortestQueryHandler.cs | 39 +++++++- .../VehicleEnrollmentSearchCompanyDto.cs | 25 +++++ .../VehicleEnrollmentSearchVehicleDto.cs | 94 +++++++++++++++++++ ...cleEnrollmentSearchVehicleEnrollmentDto.cs | 4 + 5 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs create mode 100644 src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs index 4cd7fa8..f25ba6c 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -12,6 +12,7 @@ namespace cuqmbr.TravelGuide.Application .VehicleEnrollmentSearch.Queries.SearchAll; // TODO: Add configurable time between transfers. +// TODO: Refactor DTO creation code to use mapper as much as possible. public class SearchAllQueryHandler : IRequestHandler> { @@ -71,8 +72,24 @@ public class SearchAllQueryHandler : 1, int.MaxValue, cancellationToken)) .Items; + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)), + 1, int.MaxValue, cancellationToken)) + .Items; - // Hydrate vehicle enrollments with route address details + var companyIds = vehicles.Select(e => e.CompanyId); + var companies = (await _unitOfWork.CompanyRepository + .GetPageAsync( + e => companyIds.Contains(e.Id), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with: + // - route address details; + // - vehicle info; + // - comapny info. foreach (var vehicleEnrollment in vehicleEnrollments) { @@ -80,6 +97,12 @@ public class SearchAllQueryHandler : .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) .OrderBy(e => e.RouteAddress.Order) .ToArray(); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.Vehicle.Company = companies + .Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId); } @@ -310,7 +333,13 @@ public class SearchAllQueryHandler : VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, - Addresses = addressDtos + Addresses = addressDtos, + Company = _mapper + .Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper + .Map( + tag.VehicleEnrollment.Vehicle) }); @@ -443,7 +472,11 @@ public class SearchAllQueryHandler : VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, - Addresses = addressDtos + Addresses = addressDtos, + Company = _mapper.Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper.Map( + tag.VehicleEnrollment.Vehicle) }); // --------------- diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs index c1acabb..afbcc45 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -12,6 +12,7 @@ namespace cuqmbr.TravelGuide.Application .VehicleEnrollmentSearch.Queries.SearchShortest; // TODO: Add configurable time between transfers. +// TODO: Refactor DTO creation code to use mapper as much as possible. public class SearchShortestQueryHandler : IRequestHandler { @@ -71,8 +72,24 @@ public class SearchShortestQueryHandler : 1, int.MaxValue, cancellationToken)) .Items; + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)), + 1, int.MaxValue, cancellationToken)) + .Items; - // Hydrate vehicle enrollments with route address details + var companyIds = vehicles.Select(e => e.CompanyId); + var companies = (await _unitOfWork.CompanyRepository + .GetPageAsync( + e => companyIds.Contains(e.Id), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with: + // - route address details; + // - vehicle info; + // - comapny info. foreach (var vehicleEnrollment in vehicleEnrollments) { @@ -80,6 +97,12 @@ public class SearchShortestQueryHandler : .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) .OrderBy(e => e.RouteAddress.Order) .ToArray(); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.Vehicle.Company = companies + .Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId); } @@ -300,7 +323,13 @@ public class SearchShortestQueryHandler : VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, - Addresses = addressDtos + Addresses = addressDtos, + Company = _mapper + .Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper + .Map( + tag.VehicleEnrollment.Vehicle) }); firstRouteAddressId = nextTag.RouteAddressId; @@ -422,7 +451,11 @@ public class SearchShortestQueryHandler : VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, - Addresses = addressDtos + Addresses = addressDtos, + Company = _mapper.Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper.Map( + tag.VehicleEnrollment.Vehicle) }); } else diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs new file mode 100644 index 0000000..40e5155 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchCompanyDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs new file mode 100644 index 0000000..acc05f4 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs @@ -0,0 +1,94 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs index 5481f76..37c959d 100644 --- a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs @@ -24,6 +24,10 @@ public sealed class VehicleEnrollmentSearchVehicleEnrollmentDto public short Order { get; set; } + public VehicleEnrollmentSearchCompanyDto Company { get; set; } + + public VehicleEnrollmentSearchVehicleDto Vehicle { get; set; } + public ICollection Addresses { get; set; } } From 4ae17c5a91403cecf72dfaa218a0eb0abce7db7e Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 27 May 2025 18:10:53 +0300 Subject: [PATCH 23/35] add vehicle enrollment employee management --- .../VehicleEnrollmentEmployeeRepository.cs | 7 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../AddVehicleEnrollmentCommand.cs | 2 + .../AddVehicleEnrollmentCommandHandler.cs | 22 + .../AddVehicleEnrollmentCommandValidator.cs | 67 +- .../UpdateVehicleEnrollmentCommand.cs | 2 + .../UpdateVehicleEnrollmentCommandHandler.cs | 48 + ...UpdateVehicleEnrollmentCommandValidator.cs | 68 +- .../GetVehicleEnrollmentQueryHandler.cs | 30 +- .../GetVehicleEnrollmentsPageQuery.cs | 2 + .../GetVehicleEnrollmentsPageQueryHandler.cs | 251 ++-- .../VehicleEnrollmentDto.cs | 21 +- .../VehicleEnrollmentVehicleDto.cs | 94 ++ .../AddVehicleEnrollmentViewModel.cs | 2 + ...etVehicleEnrollmentsPageFilterViewModel.cs | 2 + .../UpdateVehicleEnrollmentViewModel.cs | 2 + src/Domain/Entities/Employee.cs | 2 + src/Domain/Entities/VehicleEnrollment.cs | 3 +- .../Entities/VehicleEnrollmentEmployee.cs | 13 + .../VehicleEnrollmentsController.cs | 9 +- .../InMemory/InMemoryUnitOfWork.cs | 4 + ...moryVehicleEnrollmentEmployeeRepository.cs | 13 + .../VehicleEnrollmentEmployeeConfiguration.cs | 70 ++ ...dd_Vehicle_Enrollment_Employee.Designer.cs | 1083 +++++++++++++++++ ...7113429_Add_Vehicle_Enrollment_Employee.cs | 73 ++ .../PostgreSqlDbContextModelSnapshot.cs | 64 + .../PostgreSql/PostgreSqlUnitOfWork.cs | 4 + ...eSqlVehicleEnrollmentEmployeeRepository.cs | 13 + 28 files changed, 1829 insertions(+), 144 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs create mode 100644 src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs create mode 100644 src/Domain/Entities/VehicleEnrollmentEmployee.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..6c396d2 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface VehicleEnrollmentEmployeeRepository : + BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index de5c4ee..262822a 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -36,6 +36,8 @@ public interface UnitOfWork : IDisposable RouteAddressDetailRepository RouteAddressDetailRepository { get; } + VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs index 8f5c49e..72ff69b 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs @@ -17,4 +17,6 @@ public record AddVehicleEnrollmentCommand : IRequest public Guid RouteGuid { get; set; } public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeGuids { get; set; } } diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs index 9688d98..b5d8d9c 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -158,6 +158,20 @@ public class AddVehicleEnrollmentCommandHandler : } + // Check if employee guids are valid + + var employees = (await _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + 1, request.EmployeeGuids.Count, cancellationToken)) + .Items; + + if (employees.Count < request.EmployeeGuids.Count) + { + throw new NotFoundException(); + } + + // Create entity and add to datastore. var entity = new VehicleEnrollment() @@ -166,6 +180,8 @@ public class AddVehicleEnrollmentCommandHandler : Currency = request.Currency, VehicleId = vehicle.Id, RouteId = route.Id, + Route = route, + Vehicle = vehicle, RouteAddressDetails = route.RouteAddresses .OrderBy(ra => ra.Order) .Select(ra => new RouteAddressDetail() @@ -181,6 +197,12 @@ public class AddVehicleEnrollmentCommandHandler : .CurrentAddressStopTime, RouteAddressId = ra.Id }) + .ToArray(), + VehicleEnrollmentEmployees = request.EmployeeGuids.Select(g => + new VehicleEnrollmentEmployee() + { + EmployeeId = employees.Single(e => e.Guid == g).Id + }) .ToArray() }; diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs index c89f266..1df07c1 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs @@ -1,3 +1,4 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; @@ -17,12 +18,12 @@ public class AddVehicleEnrollmentCommandValidator : RuleFor(v => v.DepartureTime) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]) - .Must(dt => dt >= DateTimeOffset.Now) - .WithMessage( - String.Format( - cultureService.Culture, - localizer["FluentValidation.DateTimeOffset.GreaterThanOrEqualTo"], - DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + .GreaterThanOrEqualTo(DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); RuleFor(v => v.Currency) .Must(c => Currency.Enumerations.ContainsValue(c)) @@ -41,16 +42,36 @@ public class AddVehicleEnrollmentCommandValidator : localizer["FluentValidation.GreaterThanOrEqualTo"], 2)); - RuleFor(v => v.RouteAddressDetails) - .Must(v => v.All(rad => rad.RouteAddressGuid != Guid.Empty)) - .WithMessage(localizer["FluentValidation.NotEmpty"]) - .Must(v => v.All(rad => rad.TimeToNextAddress >= TimeSpan.Zero)) - .WithMessage(localizer["VehicleEnrollments.NegativeTime"]) - .Must(v => v.All(rad => rad.CostToNextAddress >= 0)) - .WithMessage(localizer["VehicleEnrollments.NegativeCost"]) - .Must(v => v.All(rad => rad.CurrentAddressStopTime >= TimeSpan.Zero)) - .WithMessage(localizer["VehicleEnrollments.NegativeTime"]); + RuleForEach(v => v.RouteAddressDetails).ChildRules(rad => + { + rad.RuleFor(rad => rad.RouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + rad.RuleFor(rad => rad.TimeToNextAddress) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + + rad.RuleFor(rad => rad.CostToNextAddress) + .GreaterThanOrEqualTo(0) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 0)); + + rad.RuleFor(rad => rad.CurrentAddressStopTime) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + }); RuleFor(v => v.VehicleGuid) .NotEmpty() @@ -59,5 +80,21 @@ public class AddVehicleEnrollmentCommandValidator : RuleFor(v => v.RouteGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.EmployeeGuids.Count) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.EmployeeGuids) + .IsUnique(g => g) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.EmployeeGuids) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs index 918eda7..3fff097 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs @@ -15,4 +15,6 @@ public record UpdateVehicleEnrollmentCommand : IRequest public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeGuids { get; set; } } diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs index d562c45..fb3aed7 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -136,8 +136,56 @@ public class UpdateVehicleEnrollmentCommandHandler : } + // Check if employee guids are valid + + var employees = (await _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + 1, request.EmployeeGuids.Count, cancellationToken)) + .Items; + + if (employees.Count < request.EmployeeGuids.Count) + { + throw new NotFoundException(); + } + + + // Get vehicle and hydrate vehicle enrollment + + var vehicle = await _unitOfWork.VehicleRepository + .GetOneAsync(e => e.Id == entity.VehicleId, cancellationToken); + entity.Vehicle = vehicle; + // Update entity and add to datastore. + var requestEmployeeGuids = request.EmployeeGuids; + var datastoreEmployeeGuids = (await _unitOfWork + .VehicleEnrollmentEmployeeRepository.GetPageAsync( + e => e.VehicleEnrollmentId == entity.Id, + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items + .Select(e => e.Employee.Guid); + + var commonEmployeeGuids = datastoreEmployeeGuids + .Intersect(requestEmployeeGuids); + + var newEmployeeGuids = requestEmployeeGuids + .Except(datastoreEmployeeGuids); + + var combinedEmployeeGuids = commonEmployeeGuids + .Union(newEmployeeGuids); + + entity.VehicleEnrollmentEmployees = combinedEmployeeGuids + .Select(g => + new VehicleEnrollmentEmployee() + { + EmployeeId = employees.Single(e => e.Guid == g).Id, + VehicleEnrollmentId = entity.Id + }) + .ToList(); + + entity.DepartureTime = request.DepartureTime; entity.Currency = request.Currency; diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs index 1cd8966..2530e8c 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs @@ -1,3 +1,4 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; @@ -21,12 +22,12 @@ public class UpdateVehicleEnrollmentCommandValidator : RuleFor(v => v.DepartureTime) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]) - .Must(dt => dt >= DateTimeOffset.Now) - .WithMessage( - String.Format( - cultureService.Culture, - localizer["FluentValidation.DateTimeOffset.GreaterThanOrEqualTo"], - DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + .GreaterThanOrEqualTo(DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); RuleFor(v => v.Currency) .Must(c => Currency.Enumerations.ContainsValue(c)) @@ -45,14 +46,51 @@ public class UpdateVehicleEnrollmentCommandValidator : localizer["FluentValidation.GreaterThanOrEqualTo"], 2)); - RuleFor(v => v.RouteAddressDetails) - .Must(v => v.All(rad => rad.RouteAddressGuid != Guid.Empty)) - .WithMessage(localizer["FluentValidation.NotEmpty"]) - .Must(v => v.All(rad => rad.TimeToNextAddress >= TimeSpan.Zero)) - .WithMessage(localizer["VehicleEnrollments.NegativeTime"]) - .Must(v => v.All(rad => rad.CostToNextAddress >= 0)) - .WithMessage(localizer["VehicleEnrollments.NegativeCost"]) - .Must(v => v.All(rad => rad.CurrentAddressStopTime >= TimeSpan.Zero)) - .WithMessage(localizer["VehicleEnrollments.NegativeTime"]); + RuleForEach(v => v.RouteAddressDetails).ChildRules(rad => + { + rad.RuleFor(rad => rad.RouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + rad.RuleFor(rad => rad.TimeToNextAddress) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + + rad.RuleFor(rad => rad.CostToNextAddress) + .GreaterThanOrEqualTo(0) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 0)); + + rad.RuleFor(rad => rad.CurrentAddressStopTime) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + }); + + RuleFor(v => v.EmployeeGuids.Count) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.EmployeeGuids) + .IsUnique(g => g) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.EmployeeGuids) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs index 5cb3804..3b1ad69 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs @@ -42,24 +42,44 @@ public class GetVehicleEnrollmentQueryHandler : throw new NotFoundException(); } - // Hydrate vehicle enrollment with address information + // Hydrate vehicle enrollment with address, + // vehicle, route and employee information. - var routeAddresses = await _unitOfWork.RouteAddressRepository + var routeAddresses = (await _unitOfWork.RouteAddressRepository .GetPageAsync( e => entity.RouteAddressDetails .Select(rad => rad.RouteAddressId) .Contains(e.Id), e => e.Address.City.Region.Country, - 1, entity.RouteAddressDetails.Count(), - cancellationToken); + 1, entity.RouteAddressDetails.Count(), cancellationToken)) + .Items; foreach (var rad in entity.RouteAddressDetails) { - rad.RouteAddress = routeAddresses.Items + rad.RouteAddress = routeAddresses .First(ra => ra.Id == rad.RouteAddressId); } + var vehicle = await _unitOfWork.VehicleRepository + .GetOneAsync(e => e.Id == entity.VehicleId, cancellationToken); + + var route = await _unitOfWork.RouteRepository + .GetOneAsync(e => e.Id == entity.RouteId, cancellationToken); + + var vehicleEnrollmentEmployees = + (await _unitOfWork.VehicleEnrollmentEmployeeRepository + .GetPageAsync( + e => e.VehicleEnrollmentId == entity.Id, + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items; + + entity.Vehicle = vehicle; + entity.Route = route; + entity.VehicleEnrollmentEmployees = + vehicleEnrollmentEmployees.ToArray(); + // Convert currency diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs index a2c07bb..1253cd9 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs @@ -49,4 +49,6 @@ public record GetVehicleEnrollmentsPageQuery : public decimal? CostLessThanOrEqual { get; set; } public Currency? Currency { get; set; } + + public ICollection? EmployeeGuids { get; set; } } diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs index 114aa9d..70cfc40 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs @@ -35,120 +35,162 @@ public class GetVehicleEnrollmentsPageQueryHandler : CancellationToken cancellationToken) { // TODO: Add search functionality or remove it - var paginatedList = await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( - e => - // (e.Name.ToLower().Contains(request.Search.ToLower()) || - // e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && - (request.RouteGuid != null - ? e.Route.Guid == request.RouteGuid - : true) && - (request.VehicleGuid != null - ? e.Vehicle.Guid >= request.VehicleGuid - : true) && - (request.NumberOfAddressesGreaterThanOrEqual != null - ? - e.RouteAddressDetails.Count() >= - request.NumberOfAddressesGreaterThanOrEqual - : true) && - (request.NumberOfAddressesLessThanOrEqual != null - ? - e.RouteAddressDetails.Count() <= - request.NumberOfAddressesLessThanOrEqual - : true) && - (request.DepartureTimeGreaterThanOrEqual != null - ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqual - : true) && - (request.DepartureTimeLessThanOrEqual != null - ? e.DepartureTime <= request.DepartureTimeLessThanOrEqual - : true) && - (request.ArrivalTimeGreaterThanOrEqual != null - ? - e.DepartureTime.AddSeconds(e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds + - rad.CurrentAddressStopTime.TotalSeconds)) >= - request.ArrivalTimeGreaterThanOrEqual - : true) && - (request.ArrivalTimeLessThanOrEqual != null - ? - e.DepartureTime.AddSeconds(e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds + - rad.CurrentAddressStopTime.TotalSeconds)) <= - request.ArrivalTimeLessThanOrEqual - : true) && - (request.TravelTimeGreaterThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= - request.TravelTimeGreaterThanOrEqual.Value.TotalSeconds - : true) && - (request.TravelTimeLessThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= - request.TravelTimeLessThanOrEqual.Value.TotalSeconds - : true) && - (request.TimeMovingGreaterThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= - request.TimeMovingGreaterThanOrEqual.Value.TotalSeconds - : true) && - (request.TimeMovingLessThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= - request.TimeMovingLessThanOrEqual.Value.TotalSeconds - : true) && - (request.TimeInStopsGreaterThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) >= - request.TimeInStopsGreaterThanOrEqual.Value.TotalSeconds - : true) && - (request.TimeInStopsLessThanOrEqual != null - ? - e.RouteAddressDetails - .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) <= - request.TimeInStopsLessThanOrEqual.Value.TotalSeconds - : true) && - (request.CostGreaterThanOrEqual != null - ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) >= - request.CostGreaterThanOrEqual - : true) && - (request.CostLessThanOrEqual != null - ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) <= - request.CostLessThanOrEqual - : true) && - (request.Currency != null - ? e.Currency == request.Currency - : true), - e => e.RouteAddressDetails, - request.PageNumber, request.PageSize, - cancellationToken); + var pagedList = await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + // (e.Name.ToLower().Contains(request.Search.ToLower()) || + // e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RouteGuid != null + ? e.Route.Guid == request.RouteGuid + : true) && + (request.VehicleGuid != null + ? e.Vehicle.Guid >= request.VehicleGuid + : true) && + (request.NumberOfAddressesGreaterThanOrEqual != null + ? + e.RouteAddressDetails.Count() >= + request.NumberOfAddressesGreaterThanOrEqual + : true) && + (request.NumberOfAddressesLessThanOrEqual != null + ? + e.RouteAddressDetails.Count() <= + request.NumberOfAddressesLessThanOrEqual + : true) && + (request.DepartureTimeGreaterThanOrEqual != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqual + : true) && + (request.DepartureTimeLessThanOrEqual != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqual + : true) && + (request.ArrivalTimeGreaterThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) >= + request.ArrivalTimeGreaterThanOrEqual + : true) && + (request.ArrivalTimeLessThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) <= + request.ArrivalTimeLessThanOrEqual + : true) && + (request.TravelTimeGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TravelTimeGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TravelTimeLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TravelTimeLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TimeMovingGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TimeMovingLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) >= + request.TimeInStopsGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) <= + request.TimeInStopsLessThanOrEqual.Value.TotalSeconds + : true) && + (request.CostGreaterThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) >= + request.CostGreaterThanOrEqual + : true) && + (request.CostLessThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) <= + request.CostLessThanOrEqual + : true) && + (request.Currency != null + ? e.Currency == request.Currency + : true) && + (request.EmployeeGuids != null + ? e.VehicleEnrollmentEmployees.Any(e => request.EmployeeGuids.Contains(e.Employee.Guid)) + : true), + e => e.RouteAddressDetails, + request.PageNumber, request.PageSize, cancellationToken); + + var vehicleEnrollments = pagedList.Items; + + var vehicleIds = vehicleEnrollments.Select(e => e.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => vehicleIds.Contains(e.Id), + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + var routes = (await _unitOfWork.RouteRepository + .GetPageAsync( + e => routeIds.Contains(e.Id), + 1, routeIds.Count(), cancellationToken)) + .Items; - // Hydrate vehicle enrollment with address information + // Hydrate vehicle enrollment with address, + // vehicle, route and employee information. - var routeAddressIds = paginatedList.Items + var routeAddressIds = vehicleEnrollments .SelectMany(ve => ve.RouteAddressDetails) .Select(rad => rad.RouteAddressId); - var routeAddresses = await _unitOfWork.RouteAddressRepository + var routeAddresses = (await _unitOfWork.RouteAddressRepository .GetPageAsync( e => routeAddressIds.Contains(e.Id), e => e.Address.City.Region.Country, - 1, paginatedList.Items.Sum(e => e.RouteAddressDetails.Count()), - cancellationToken); + 1, routeAddressIds.Count(), cancellationToken)) + .Items; - foreach (var vehicleEnrollment in paginatedList.Items) + var vehicleEnrollmentIds = vehicleEnrollments + .Select(ve => ve.Id); + + var vehicleEnrollmentEmployees = + (await _unitOfWork.VehicleEnrollmentEmployeeRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items; + + foreach (var vehicleEnrollment in vehicleEnrollments) { foreach (var routeAddressDetail in vehicleEnrollment.RouteAddressDetails) { - routeAddressDetail.RouteAddress = routeAddresses.Items + routeAddressDetail.RouteAddress = routeAddresses .First(ra => ra.Id == routeAddressDetail.RouteAddressId); } + + vehicleEnrollment.Route = routes + .Single(e => e.Id == vehicleEnrollment.RouteId); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.VehicleEnrollmentEmployees = + vehicleEnrollmentEmployees + .Where(vee => vee.VehicleEnrollmentId == vehicleEnrollment.Id) + .ToArray(); } @@ -158,7 +200,7 @@ public class GetVehicleEnrollmentsPageQueryHandler : if (!_sessionCurrencyService.Currency.Equals(Currency.Default)) { - foreach (var ve in paginatedList.Items) + foreach (var ve in vehicleEnrollments) { foreach (var rad in ve.RouteAddressDetails) { @@ -171,17 +213,18 @@ public class GetVehicleEnrollmentsPageQueryHandler : } - var mappedItems = _mapper - .ProjectTo(paginatedList.Items.AsQueryable()); + var vehicleEnrollmentsDto = _mapper + .Map>(vehicleEnrollments) + .AsQueryable(); - mappedItems = QueryableExtension - .ApplySort(mappedItems, request.Sort); + vehicleEnrollmentsDto = QueryableExtension + .ApplySort(vehicleEnrollmentsDto, request.Sort); _unitOfWork.Dispose(); return new PaginatedList( - mappedItems.ToList(), - paginatedList.TotalCount, request.PageNumber, + vehicleEnrollmentsDto.ToList(), + pagedList.TotalCount, request.PageNumber, request.PageSize); } } diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs index 0da091a..40ca6a3 100644 --- a/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs @@ -33,8 +33,17 @@ public sealed class VehicleEnrollmentDto : IMapFrom public string Currency { get; set; } + public Guid VehicleUuid { get; set; } + + public VehicleEnrollmentVehicleDto Vehicle { get; set; } + + public Guid RouteUuid { get; set; } + public ICollection RouteAddressDetails { get; set; } + // TODO: Add collection of employee dto objects + public ICollection EmployeeUuids { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() @@ -45,6 +54,16 @@ public sealed class VehicleEnrollmentDto : IMapFrom d => d.DepartureTime, opt => opt .MapFrom( - s => s.DepartureTime)); + s => s.DepartureTime)) + .ForMember( + d => d.VehicleUuid, + opt => opt.MapFrom(s => s.Vehicle.Guid)) + .ForMember( + d => d.RouteUuid, + opt => opt.MapFrom(s => s.Route.Guid)) + .ForMember( + d => d.EmployeeUuids, + opt => opt.MapFrom(s => + s.VehicleEnrollmentEmployees.Select(e => e.Employee.Guid))); } } diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs new file mode 100644 index 0000000..a810f0a --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs @@ -0,0 +1,94 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs index dd09bcf..deeb095 100644 --- a/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs +++ b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs @@ -12,4 +12,6 @@ public sealed class AddVehicleEnrollmentViewModel public Guid RouteUuid { get; set; } public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeUuids { get; set; } } diff --git a/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs index 5cde753..511799e 100644 --- a/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs +++ b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs @@ -43,4 +43,6 @@ public sealed class GetVehicleEnrollmentsPageFilterViewModel public decimal? CostLessThanOrEqual { get; set; } public string? Currency { get; set; } + + public ICollection? EmployeeUuids { get; set; } } diff --git a/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs index 5c61d67..e58befd 100644 --- a/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs +++ b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs @@ -8,4 +8,6 @@ public sealed class UpdateVehicleEnrollmentViewModel public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeUuids { get; set; } } diff --git a/src/Domain/Entities/Employee.cs b/src/Domain/Entities/Employee.cs index ec518ce..3035cad 100644 --- a/src/Domain/Entities/Employee.cs +++ b/src/Domain/Entities/Employee.cs @@ -20,4 +20,6 @@ public sealed class Employee : EntityBase public Company Company { get; set; } public ICollection Documents { get; set; } + + public ICollection VehicleEnrollmentEmployees { get; set; } } diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs index b6c2ca0..7cfdc46 100644 --- a/src/Domain/Entities/VehicleEnrollment.cs +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -21,9 +21,10 @@ public class VehicleEnrollment : EntityBase public ICollection RouteAddressDetails { get; set; } - public ICollection Tickets { get; set; } + public ICollection VehicleEnrollmentEmployees { get; set; } + public DateTimeOffset GetDepartureTime(long DepartureRouteAddressId) { diff --git a/src/Domain/Entities/VehicleEnrollmentEmployee.cs b/src/Domain/Entities/VehicleEnrollmentEmployee.cs new file mode 100644 index 0000000..7ac51df --- /dev/null +++ b/src/Domain/Entities/VehicleEnrollmentEmployee.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class VehicleEnrollmentEmployee : EntityBase +{ + public long EmployeeId { get; set; } + + public Employee Employee { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } +} diff --git a/src/HttpApi/Controllers/VehicleEnrollmentsController.cs b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs index b5a703c..c79fe23 100644 --- a/src/HttpApi/Controllers/VehicleEnrollmentsController.cs +++ b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs @@ -75,7 +75,8 @@ public class VehicleEnrollmentsController : ControllerBase CurrentAddressStopTime = rad.CurrentAddressStopTime, RouteAddressGuid = rad.RouteAddressUuid }) - .ToArray() + .ToArray(), + EmployeeGuids = viewModel.EmployeeUuids }, cancellationToken)); } @@ -141,7 +142,8 @@ public class VehicleEnrollmentsController : ControllerBase filterQuery.CostGreaterThanOrEqual, CostLessThanOrEqual = filterQuery.CostLessThanOrEqual, - Currency = Currency.FromName(filterQuery.Currency) + Currency = Currency.FromName(filterQuery.Currency), + EmployeeGuids = filterQuery.EmployeeUuids }, cancellationToken); } @@ -224,7 +226,8 @@ public class VehicleEnrollmentsController : ControllerBase CurrentAddressStopTime = rad.CurrentAddressStopTime, RouteAddressGuid = rad.RouteAddressUuid }) - .ToArray() + .ToArray(), + EmployeeGuids = viewModel.EmployeeUuids }, cancellationToken); } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 78c3f1a..c04be86 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -32,6 +32,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork TicketRepository = new InMemoryTicketRepository(_dbContext); RouteAddressDetailRepository = new InMemoryRouteAddressDetailRepository(_dbContext); + VehicleEnrollmentEmployeeRepository = + new InMemoryVehicleEnrollmentEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -66,6 +68,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..d9b8d21 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleEnrollmentEmployeeRepository : + InMemoryBaseRepository, + VehicleEnrollmentEmployeeRepository +{ + public InMemoryVehicleEnrollmentEmployeeRepository( + InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs new file mode 100644 index 0000000..11b319f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs @@ -0,0 +1,70 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleEnrollmentEmployeeConfiguration : + BaseConfiguration +{ + public override void Configure( + EntityTypeBuilder builder) + { + builder + .ToTable("vehicle_enrollment_employees"); + + base.Configure(builder); + + + builder + .Property(vee => vee.EmployeeId) + .HasColumnName("employee_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(vee => vee.Employee) + .WithMany(e => e.VehicleEnrollmentEmployees) + .HasForeignKey(vee => vee.EmployeeId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.EmployeeId) + .Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(vee => vee.EmployeeId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.EmployeeId) + .Metadata.GetColumnName()}"); + + + builder + .Property(vee => vee.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(vee => vee.VehicleEnrollment) + .WithMany(ve => ve.VehicleEnrollmentEmployees) + .HasForeignKey(vee => vee.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.VehicleEnrollmentId) + .Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(vee => vee.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.VehicleEnrollmentId) + .Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs new file mode 100644 index 0000000..c4ef7f4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs @@ -0,0 +1,1083 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250527113429_Add_Vehicle_Enrollment_Employee")] + partial class Add_Vehicle_Enrollment_Employee + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs new file mode 100644 index 0000000..768ef38 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Vehicle_Enrollment_Employee : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "vehicle_enrollment_employees_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicle_enrollment_employees", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicle_enrollment_employees_id_sequence')"), + employee_id = table.Column(type: "bigint", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicle_enrollment_employees", x => x.id); + table.UniqueConstraint("altk_vehicle_enrollment_employees_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_vehicle_enrollment_employees_employee_id", + column: x => x.employee_id, + principalSchema: "application", + principalTable: "employees", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_vehicle_enrollment_employees_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollment_employees_employee_id", + schema: "application", + table: "vehicle_enrollment_employees", + column: "employee_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollment_employees_vehicle_enrollment_id", + schema: "application", + table: "vehicle_enrollment_employees", + column: "vehicle_enrollment_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "vehicle_enrollment_employees", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicle_enrollment_employees_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index e4f59b1..1f16747 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -47,6 +47,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("tickets_id_sequence"); + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); modelBuilder.HasSequence("vehicles_id_sequence"); @@ -691,6 +693,43 @@ namespace Persistence.PostgreSql.Migrations }); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => { b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); @@ -950,6 +989,27 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Vehicle"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Navigation("AddressRoutes"); @@ -975,6 +1035,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => { b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => @@ -1009,6 +1071,8 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("RouteAddressDetails"); b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); }); #pragma warning restore 612, 618 } diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 6898026..717d684 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -32,6 +32,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork TicketRepository = new PostgreSqlTicketRepository(_dbContext); RouteAddressDetailRepository = new PostgreSqlRouteAddressDetailRepository(_dbContext); + VehicleEnrollmentEmployeeRepository = + new PostgreSqlVehicleEnrollmentEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -66,6 +68,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..ed6ca66 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleEnrollmentEmployeeRepository : + PostgreSqlBaseRepository, + VehicleEnrollmentEmployeeRepository +{ + public PostgreSqlVehicleEnrollmentEmployeeRepository( + PostgreSqlDbContext dbContext) + : base(dbContext) { } +} From fafb665cd283e336b725fd7be04e43d683c03b10 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 28 May 2025 12:33:49 +0300 Subject: [PATCH 24/35] rewrite identity system --- TravelGuide.sln | 6 - .../AddAddress/AddAddressCommandAuthorizer.cs | 4 +- .../AddAddress/AddAddressCommandHandler.cs | 2 +- .../AddAddress/AddAddressCommandValidator.cs | 2 +- .../DeleteAddressCommandAuthorizer.cs | 4 +- .../DeleteAddressCommandHandler.cs | 2 +- .../UpdateAddressCommandAuthorizer.cs | 4 +- .../UpdateAddressCommandHandler.cs | 2 +- .../UpdateAddressCommandValidator.cs | 2 +- .../GetAddress/GetAddressQueryAuthorizer.cs | 4 +- .../GetAddress/GetAddressQueryHandler.cs | 2 +- .../GetAddressesPageQueryAuthorizer.cs | 4 +- .../GetAddressesPageQueryHandler.cs | 2 +- .../GetAddressesPageQueryValidator.cs | 2 +- .../AddAircraftCommandAuthorizer.cs | 4 +- .../AddAircraft/AddAircraftCommandHandler.cs | 2 +- .../AddAircraftCommandValidator.cs | 2 +- .../DeleteAircraftCommandAuthorizer.cs | 4 +- .../DeleteAircraftCommandHandler.cs | 2 +- .../UpdateAircraftCommandAuthorizer.cs | 4 +- .../UpdateAircraftCommandHandler.cs | 2 +- .../UpdateAircraftCommandValidator.cs | 2 +- .../GetAircraft/GetAircraftQueryAuthorizer.cs | 4 +- .../GetAircraft/GetAircraftQueryHandler.cs | 2 +- .../GetAircraftsPageQueryAuthorizer.cs | 4 +- .../GetAircraftsPageQueryHandler.cs | 2 +- .../GetAircraftsPageQueryValidator.cs | 2 +- src/Application/Application.csproj | 3 + .../Commands/Register/RegisterCommand.cs | 2 + .../Register/RegisterCommandAuthorizer.cs | 13 + .../Register/RegisterCommandHandler.cs | 78 +- .../Register/RegisterCommandValidator.cs | 55 +- .../RenewAccessTokenCommandAuthorizer.cs | 23 +- .../RenewAccessTokenCommandHandler.cs | 85 +- .../RenewAccessTokenWithCookieCommand.cs | 6 - ...wAccessTokenWithCookieCommandAuthorizer.cs | 26 - ...enewAccessTokenWithCookieCommandHandler.cs | 28 - ...ewAccessTokenWithCookieCommandValidator.cs | 10 - .../RevokeRefreshTokenCommandAuthorizer.cs | 13 +- .../RevokeRefreshTokenCommandHandler.cs | 32 +- .../RevokeRefreshTokenWithCookieCommand.cs | 6 - ...RefreshTokenWithCookieCommandAuthorizer.cs | 26 - ...okeRefreshTokenWithCookieCommandHandler.cs | 28 - ...eRefreshTokenWithCookieCommandValidator.cs | 10 - .../Queries/Login/LoginQuery.cs | 2 +- .../Queries/Login/LoginQueryAuthorizer.cs | 12 + .../Queries/Login/LoginQueryHandler.cs | 131 +- .../Queries/Login/LoginQueryValidator.cs | 11 +- .../AddBus/AddBusCommandAuthorizer.cs | 4 +- .../Commands/AddBus/AddBusCommandHandler.cs | 2 +- .../Commands/AddBus/AddBusCommandValidator.cs | 2 +- .../DeleteBus/DeleteBusCommandAuthorizer.cs | 4 +- .../DeleteBus/DeleteBusCommandHandler.cs | 2 +- .../UpdateBus/UpdateBusCommandAuthorizer.cs | 4 +- .../UpdateBus/UpdateBusCommandHandler.cs | 2 +- .../UpdateBus/UpdateBusCommandValidator.cs | 2 +- .../Queries/GetBus/GetBusQueryAuthorizer.cs | 4 +- .../Queries/GetBus/GetBusQueryHandler.cs | 2 +- .../GetBusesPageQueryAuthorizer.cs | 4 +- .../GetBusesPage/GetBusesPageQueryHandler.cs | 2 +- .../GetBusesPageQueryValidator.cs | 2 +- .../AddCity/AddCityCommandAuthorizer.cs | 4 +- .../Commands/AddCity/AddCityCommandHandler.cs | 2 +- .../AddCity/AddCityCommandValidator.cs | 2 +- .../DeleteCity/DeleteCityCommandAuthorizer.cs | 4 +- .../DeleteCity/DeleteCityCommandHandler.cs | 2 +- .../UpdateCity/UpdateCityCommandAuthorizer.cs | 4 +- .../UpdateCity/UpdateCityCommandHandler.cs | 2 +- .../UpdateCity/UpdateCityCommandValidator.cs | 2 +- .../GetCitiesPageQueryAuthorizer.cs | 4 +- .../GetCitiesPageQueryHandler.cs | 2 +- .../GetCitiesPageQueryValidator.cs | 2 +- .../Queries/GetCity/GetCityQueryAuthorizer.cs | 4 +- .../Queries/GetCity/GetCityQueryHandler.cs | 2 +- .../Authorization/MustBeInRolesRequirement.cs | 2 +- .../FluentValidation/CustomValidators.cs | 8 + .../Services/AuthenticationService.cs | 18 - .../Interfaces/Services/SessionUserService.cs | 22 - .../Resolvers/DateTimeOffsetResolver.cs | 2 +- .../Repositories/AccountRepository.cs | 5 + .../Repositories/AccountRoleRepository.cs | 5 + .../Repositories/AddressRepository.cs | 3 +- .../Repositories/AircraftRepository.cs | 3 +- .../Repositories/BaseRepository.cs | 3 +- .../Persistence/Repositories/BusRepository.cs | 3 +- .../Repositories/CityRepository.cs | 3 +- .../Repositories/CompanyRepository.cs | 2 +- .../Repositories/CountryRepository.cs | 3 +- .../Repositories/EmployeeRepository.cs | 2 +- .../Repositories/RefreshTokenRepository.cs | 5 + .../Repositories/RegionRepository.cs | 3 +- .../Repositories/RoleRepository.cs | 5 + .../RouteAddressDetailRepository.cs | 2 +- .../Repositories/RouteAddressRepository.cs | 3 +- .../Repositories/RouteRepository.cs | 3 +- .../Repositories/TicketGroupRepository.cs | 2 +- .../Repositories/TicketRepository.cs | 2 +- .../Repositories/TrainRepository.cs | 3 +- .../VehicleEnrollmentEmployeeRepository.cs | 3 +- .../VehicleEnrollmentRepository.cs | 3 +- .../Repositories/VehicleRepository.cs | 3 +- .../Persistence/UnitOfWork.cs | 16 +- .../Services/CurrencyConverterService.cs | 2 +- .../Services/LiqPayPaymentService.cs | 2 +- .../Common/Services/PasswordHasherService.cs | 10 + .../Services/SessionCurrencyService.cs | 2 +- .../Services/SessionTimeZoneService.cs | 2 +- .../Common/Services/SessionUserService.cs | 22 + .../Services/SessoionCultureService.cs | 2 +- .../AddCompany/AddCompanyCommandAuthorizer.cs | 4 +- .../AddCompany/AddCompanyCommandHandler.cs | 2 +- .../AddCompany/AddCompanyCommandValidator.cs | 2 +- .../DeleteCompanyCommandAuthorizer.cs | 4 +- .../DeleteCompanyCommandHandler.cs | 2 +- .../UpdateCompanyCommandAuthorizer.cs | 4 +- .../UpdateCompanyCommandHandler.cs | 2 +- .../UpdateCompanyCommandValidator.cs | 2 +- .../GetCompaniesPageQueryAuthorizer.cs | 4 +- .../GetCompaniesPageQueryHandler.cs | 2 +- .../GetCompaniesPageQueryValidator.cs | 2 +- .../GetCompany/GetCompanyQueryAuthorizer.cs | 4 +- .../GetCompany/GetCompanyQueryHandler.cs | 2 +- src/Application/ConfigurationOptions.cs | 15 + .../AddCountry/AddCountryCommandAuthorizer.cs | 4 +- .../AddCountry/AddCountryCommandHandler.cs | 2 +- .../AddCountry/AddCountryCommandValidator.cs | 2 +- .../DeleteCountryCommandAuthorizer.cs | 4 +- .../DeleteCountryCommandHandler.cs | 2 +- .../UpdateCountryCommandAuthorizer.cs | 4 +- .../UpdateCountryCommandHandler.cs | 2 +- .../UpdateCountryCommandValidator.cs | 2 +- .../GetCountriesPageQueryAuthorizer.cs | 4 +- .../GetCountriesPageQueryHandler.cs | 2 +- .../GetCountriesPageQueryValidator.cs | 2 +- .../GetCountry/GetCountryQueryAuthorizer.cs | 4 +- .../GetCountry/GetCountryQueryHandler.cs | 2 +- .../AddEmployeeCommandAuthorizer.cs | 4 +- .../AddEmployee/AddEmployeeCommandHandler.cs | 2 +- .../AddEmployeeCommandValidator.cs | 2 +- .../DeleteEmployeeCommandAuthorizer.cs | 4 +- .../DeleteEmployeeCommandHandler.cs | 2 +- .../UpdateEmployeeCommandAuthorizer.cs | 4 +- .../UpdateEmployeeCommandHandler.cs | 2 +- .../UpdateEmployeeCommandValidator.cs | 2 +- .../GetEmployee/GetEmployeeQueryAuthorizer.cs | 4 +- .../GetEmployee/GetEmployeeQueryHandler.cs | 2 +- .../GetEmployeesPageQueryAuthorizer.cs | 4 +- .../GetEmployeesPageQueryHandler.cs | 2 +- .../GetEmployeesPageQueryValidator.cs | 2 +- .../Identity/Accounts/AccountDto.cs | 27 + .../Commands/AddAccount/AddAccountCommand.cs | 13 + .../AddAccount/AddAccountCommandAuthorizer.cs | 31 + .../AddAccount/AddAccountCommandHandler.cs | 77 + .../AddAccount/AddAccountCommandValidator.cs | 36 + .../ViewModels/AddAccountViewModel.cs | 10 + .../Queries/GetRolesPage/GetRolesPageQuery.cs | 13 + .../GetRolesPageQueryAuthorizer.cs | 31 + .../GetRolesPage/GetRolesPageQueryHandler.cs | 25 + .../GetRolesPageQueryValidator.cs | 43 + .../GetPaymentLinkCommandAuthorizer.cs | 4 +- .../GetPaymentLinkCommandHandler.cs | 4 +- .../GetPaymentLinkCommandValidator.cs | 2 +- .../ProcessCallbackCommandHandler.cs | 4 +- .../ProcessCallbackCommandValidator.cs | 2 +- .../AddRegion/AddRegionCommandAuthorizer.cs | 4 +- .../AddRegion/AddRegionCommandHandler.cs | 2 +- .../AddRegion/AddRegionCommandValidator.cs | 2 +- .../DeleteRegionCommandAuthorizer.cs | 4 +- .../DeleteRegionCommandHandler.cs | 2 +- .../UpdateRegionCommandAuthorizer.cs | 4 +- .../UpdateRegionCommandHandler.cs | 2 +- .../UpdateRegionCommandValidator.cs | 2 +- .../GetRegion/GetRegionQueryAuthorizer.cs | 4 +- .../GetRegion/GetRegionQueryHandler.cs | 2 +- .../GetRegionsPageQueryAuthorizer.cs | 4 +- .../GetRegionsPageQueryHandler.cs | 2 +- .../GetRegionsPageQueryValidator.cs | 2 +- .../Resources/Localization/en-US.json | 6 +- .../AddRoute/AddRouteCommandAuthorizer.cs | 4 +- .../AddRoute/AddRouteCommandHandler.cs | 2 +- .../AddRoute/AddRouteCommandValidator.cs | 2 +- .../DeleteRouteCommandAuthorizer.cs | 4 +- .../DeleteRoute/DeleteRouteCommandHandler.cs | 2 +- .../UpdateRouteCommandAuthorizer.cs | 4 +- .../UpdateRoute/UpdateRouteCommandHandler.cs | 2 +- .../UpdateRouteCommandValidator.cs | 2 +- .../GetRoute/GetRouteQueryAuthorizer.cs | 4 +- .../Queries/GetRoute/GetRouteQueryHandler.cs | 2 +- .../GetRoutesPageQueryAuthorizer.cs | 4 +- .../GetRoutesPageQueryHandler.cs | 2 +- .../GetRoutesPageQueryValidator.cs | 2 +- .../AddTicketGroupCommandAuthorizer.cs | 4 +- .../AddTicketGroupCommandHandler.cs | 4 +- .../AddTicketGroupCommandValidator.cs | 2 +- ...veOldReservedTicketGroupsCommandHandler.cs | 2 +- .../AddTrain/AddTrainCommandAuthorizer.cs | 4 +- .../AddTrain/AddTrainCommandHandler.cs | 2 +- .../AddTrain/AddTrainCommandValidator.cs | 2 +- .../DeleteTrainCommandAuthorizer.cs | 4 +- .../DeleteTrain/DeleteTrainCommandHandler.cs | 2 +- .../UpdateTrainCommandAuthorizer.cs | 4 +- .../UpdateTrain/UpdateTrainCommandHandler.cs | 2 +- .../UpdateTrainCommandValidator.cs | 2 +- .../GetTrain/GetTrainQueryAuthorizer.cs | 4 +- .../Queries/GetTrain/GetTrainQueryHandler.cs | 2 +- .../GetTrainsPageQueryAuthorizer.cs | 4 +- .../GetTrainsPageQueryHandler.cs | 2 +- .../GetTrainsPageQueryValidator.cs | 2 +- .../SearchAll/SearchAllQueryAuthorizer.cs | 4 +- .../SearchAll/SearchAllQueryHandler.cs | 4 +- .../SearchAll/SearchAllQueryValidator.cs | 2 +- .../SearchShortestQueryAuthorizer.cs | 4 +- .../SearchShortestQueryHandler.cs | 4 +- .../SearchShortestQueryValidator.cs | 2 +- .../AddVehicleEnrollmentCommandAuthorizer.cs | 4 +- .../AddVehicleEnrollmentCommandHandler.cs | 2 +- .../AddVehicleEnrollmentCommandValidator.cs | 2 +- ...eleteVehicleEnrollmentCommandAuthorizer.cs | 4 +- .../DeleteVehicleEnrollmentCommandHandler.cs | 2 +- ...pdateVehicleEnrollmentCommandAuthorizer.cs | 4 +- .../UpdateVehicleEnrollmentCommandHandler.cs | 2 +- ...UpdateVehicleEnrollmentCommandValidator.cs | 2 +- .../GetVehicleEnrollmentQueryAuthorizer.cs | 4 +- .../GetVehicleEnrollmentQueryHandler.cs | 4 +- ...etVehicleEnrollmentsPageQueryAuthorizer.cs | 4 +- .../GetVehicleEnrollmentsPageQueryHandler.cs | 4 +- ...GetVehicleEnrollmentsPageQueryValidator.cs | 2 +- src/Application/packages.lock.json | 67 + .../Application/Configuration.cs | 47 +- .../Configuration/Configuration.cs | 6 - src/Configuration/Identity/Configuration.cs | 99 -- .../Infrastructure/Configuration.cs | 7 +- .../Persistence/Configuration.cs | 9 +- src/Configuration/packages.lock.json | 89 +- src/Domain/Entities/Account.cs | 16 + src/Domain/Entities/AccountRole.cs | 12 + src/Domain/Entities/RefreshToken.cs | 20 + src/Domain/Entities/Role.cs | 10 + .../Models => Domain/Enums}/IdentityRole.cs | 16 +- .../Controllers/AuthenticationController.cs | 43 - src/HttpApi/Controllers/IdentityController.cs | 242 +++ src/HttpApi/Controllers/TestsController.cs | 4 +- .../ThreadCultureSetterMiddleware.cs | 2 +- src/HttpApi/Program.cs | 7 +- .../Services/AspNetSessionCultureService.cs | 2 +- .../Services/AspNetSessionCurrencyService.cs | 2 +- .../Services/AspNetSessionTimeZoneService.cs | 2 +- .../Services/AspNetSessionUserService.cs | 17 +- src/HttpApi/appsettings.Development.json | 20 +- src/HttpApi/appsettings.json | 20 +- src/HttpApi/packages.lock.json | 90 +- src/Identity/ConfigurationOptions.cs | 34 - .../UnSupportedDatastoreException.cs | 11 - src/Identity/Identity.csproj | 27 - src/Identity/IdentitySeeder.cs | 85 -- src/Identity/Models/IdentityRole.cs | 6 - src/Identity/Models/IdentityUser.cs | 8 - src/Identity/Models/RefreshToken.cs | 23 - .../IdentityRoleClaimConfiguration.cs | 30 - .../IdentityRoleConfiguration.cs | 34 - .../IdentityUserClaimConfiguration.cs | 30 - .../IdentityUserConfiguration.cs | 128 -- .../IdentityUserLoginConfiguration.cs | 30 - .../IdentityUserRoleConfiguration.cs | 22 - .../IdentityUserTokenConfiguration.cs | 30 - ...250423194315_Initial_migration.Designer.cs | 355 ----- .../20250423194315_Initial_migration.cs | 281 ---- ...ostgreSqlIdentityDbContextModelSnapshot.cs | 352 ----- .../PostgreSql/PostgreSqlIdentityDbContext.cs | 35 - .../PostgreSql/PostgreSqlInitializer.cs | 28 - .../Services/JwtAuthenticationService.cs | 210 --- src/Identity/packages.lock.json | 612 -------- .../ExchangeApiCurrencyConverterService.cs | 2 +- .../Services/LiqPayPaymentService.cs | 2 +- .../Services/Pbkdf2PasswordHasherService.cs | 32 + src/Infrastructure/packages.lock.json | 67 + src/Persistence/DbSeeder.cs | 99 +- src/Persistence/InMemory/InMemoryDbContext.cs | 6 + .../InMemory/InMemoryUnitOfWork.cs | 18 +- .../Repositories/InMemoryAccountRepository.cs | 11 + .../InMemoryAccountRoleRepository.cs | 11 + .../Repositories/InMemoryAddressRepository.cs | 2 +- .../InMemoryAircraftRepository.cs | 2 +- .../Repositories/InMemoryBaseRepository.cs | 2 +- .../Repositories/InMemoryBusRepository.cs | 2 +- .../Repositories/InMemoryCityRepository.cs | 2 +- .../Repositories/InMemoryCompanyRepository.cs | 2 +- .../Repositories/InMemoryCountryRepository.cs | 2 +- .../InMemoryEmployeeRepository.cs | 2 +- .../InMemoryRefreshTokenRepository.cs | 11 + .../Repositories/InMemoryRegionRepository.cs | 2 +- .../Repositories/InMemoryRoleRepository.cs | 11 + .../InMemoryRouteAddressDetailRepository.cs | 2 +- .../InMemoryRouteAddressRepository.cs | 2 +- .../Repositories/InMemoryRouteRepository.cs | 2 +- .../InMemoryTicketGroupRepository.cs | 2 +- .../Repositories/InMemoryTicketRepository.cs | 2 +- .../Repositories/InMemoryTrainRepository.cs | 2 +- ...moryVehicleEnrollmentEmployeeRepository.cs | 2 +- .../InMemoryVehicleEnrollmentRepository.cs | 2 +- .../Repositories/InMemoryVehicleRepository.cs | 2 +- .../Configurations/AccountConfiguration.cs | 48 + .../AccountRoleConfiguration.cs | 64 + .../Configurations/BaseConfiguration.cs | 2 - .../RefreshTokenConfiguration.cs | 67 + .../Configurations/RoleConfiguration.cs | 32 + ...d_Account_Role_and_AccountRole.Designer.cs | 1294 +++++++++++++++++ ...083243_Add_Account_Role_and_AccountRole.cs | 175 +++ .../PostgreSqlDbContextModelSnapshot.cs | 211 +++ .../PostgreSql/PostgreSqlDbContext.cs | 6 + .../PostgreSql/PostgreSqlUnitOfWork.cs | 18 +- .../PostgreSqlAccountRepository.cs | 11 + .../PostgreSqlAccountRoleRepository.cs | 11 + .../PostgreSqlAddressRepository.cs | 2 +- .../PostgreSqlAircraftRepository.cs | 2 +- .../Repositories/PostgreSqlBaseRepository.cs | 2 +- .../Repositories/PostgreSqlBusRepository.cs | 2 +- .../Repositories/PostgreSqlCityRepository.cs | 2 +- .../PostgreSqlCompanyRepository.cs | 2 +- .../PostgreSqlCountryRepository.cs | 2 +- .../PostgreSqlEmployeeRepository.cs | 2 +- .../PostgreSqlRefreshTokenRepository.cs | 11 + .../PostgreSqlRegionRepository.cs | 2 +- .../Repositories/PostgreSqlRoleRepository.cs | 11 + .../PostgreSqlRouteAddressDetailRepository.cs | 2 +- .../PostgreSqlRouteAddressRepository.cs | 2 +- .../Repositories/PostgreSqlRouteRepository.cs | 2 +- .../PostgreSqlTicketGroupRepository.cs | 2 +- .../PostgreSqlTicketRepository.cs | 2 +- .../Repositories/PostgreSqlTrainRepository.cs | 2 +- ...eSqlVehicleEnrollmentEmployeeRepository.cs | 2 +- .../PostgreSqlVehicleEnrollmentRepository.cs | 2 +- .../PostgreSqlVehicleRepository.cs | 2 +- .../TypeConverters/RoleConverter.cs | 13 + src/Persistence/packages.lock.json | 67 + tst/Application.IntegrationTests/BaseTest.cs | 9 +- .../CitiesTests.cs | 3 +- .../CountriesTests.cs | 3 +- .../RegionsTests.cs | 3 +- .../packages.lock.json | 90 +- 340 files changed, 3945 insertions(+), 3326 deletions(-) create mode 100644 src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs delete mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs delete mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs delete mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs delete mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs delete mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs delete mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs delete mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs delete mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs create mode 100644 src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs delete mode 100644 src/Application/Common/Interfaces/Services/AuthenticationService.cs delete mode 100644 src/Application/Common/Interfaces/Services/SessionUserService.cs create mode 100644 src/Application/Common/Persistence/Repositories/AccountRepository.cs create mode 100644 src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs rename src/Application/Common/{Interfaces => }/Persistence/Repositories/AddressRepository.cs (54%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/AircraftRepository.cs (55%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/BaseRepository.cs (94%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/BusRepository.cs (52%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/CityRepository.cs (53%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/CompanyRepository.cs (70%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/CountryRepository.cs (54%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/EmployeeRepository.cs (70%) create mode 100644 src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs rename src/Application/Common/{Interfaces => }/Persistence/Repositories/RegionRepository.cs (54%) create mode 100644 src/Application/Common/Persistence/Repositories/RoleRepository.cs rename src/Application/Common/{Interfaces => }/Persistence/Repositories/RouteAddressDetailRepository.cs (73%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/RouteAddressRepository.cs (57%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/RouteRepository.cs (53%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/TicketGroupRepository.cs (71%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/TicketRepository.cs (70%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/TrainRepository.cs (53%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs (62%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/VehicleEnrollmentRepository.cs (59%) rename src/Application/Common/{Interfaces => }/Persistence/Repositories/VehicleRepository.cs (54%) rename src/Application/Common/{Interfaces => }/Persistence/UnitOfWork.cs (73%) rename src/Application/Common/{Interfaces => }/Services/CurrencyConverterService.cs (83%) rename src/Application/Common/{Interfaces => }/Services/LiqPayPaymentService.cs (82%) create mode 100644 src/Application/Common/Services/PasswordHasherService.cs rename src/Application/Common/{Interfaces => }/Services/SessionCurrencyService.cs (64%) rename src/Application/Common/{Interfaces => }/Services/SessionTimeZoneService.cs (55%) create mode 100644 src/Application/Common/Services/SessionUserService.cs rename src/Application/Common/{Interfaces => }/Services/SessoionCultureService.cs (62%) create mode 100644 src/Application/Identity/Accounts/AccountDto.cs create mode 100644 src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs create mode 100644 src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs create mode 100644 src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs create mode 100644 src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs create mode 100644 src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs create mode 100644 src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs create mode 100644 src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs create mode 100644 src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs create mode 100644 src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs delete mode 100644 src/Configuration/Identity/Configuration.cs create mode 100644 src/Domain/Entities/Account.cs create mode 100644 src/Domain/Entities/AccountRole.cs create mode 100644 src/Domain/Entities/RefreshToken.cs create mode 100644 src/Domain/Entities/Role.cs rename src/{Application/Common/Models => Domain/Enums}/IdentityRole.cs (52%) create mode 100644 src/HttpApi/Controllers/IdentityController.cs delete mode 100644 src/Identity/ConfigurationOptions.cs delete mode 100644 src/Identity/Exceptions/UnSupportedDatastoreException.cs delete mode 100644 src/Identity/Identity.csproj delete mode 100644 src/Identity/IdentitySeeder.cs delete mode 100644 src/Identity/Models/IdentityRole.cs delete mode 100644 src/Identity/Models/IdentityUser.cs delete mode 100644 src/Identity/Models/RefreshToken.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs delete mode 100644 src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs delete mode 100644 src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs delete mode 100644 src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs delete mode 100644 src/Identity/Services/JwtAuthenticationService.cs delete mode 100644 src/Identity/packages.lock.json create mode 100644 src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs create mode 100644 src/Persistence/TypeConverters/RoleConverter.cs diff --git a/TravelGuide.sln b/TravelGuide.sln index f012d1c..75d3e9c 100644 --- a/TravelGuide.sln +++ b/TravelGuide.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Identity\Identity.csproj", "{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}" @@ -48,10 +46,6 @@ Global {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = Release|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.Build.0 = Release|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs index eb8ae61..6c88c27 100644 --- a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs index 5aaf4f9..189c8d0 100644 --- a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs index 59adff1..e1c38c8 100644 --- a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs index e8e04ca..7b5bb31 100644 --- a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs index 428b7fa..fc6ea4c 100644 --- a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs index 1063a38..80860d1 100644 --- a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs index eba7089..17ab45f 100644 --- a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs index ceae48d..adf0322 100644 --- a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs index 545dd25..d6cfa6c 100644 --- a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs index f0f5a86..730b248 100644 --- a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs index b8d21da..6521dde 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs index 9b4cd25..4fcce04 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs index b1eb480..ce7adfe 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs index 8c98851..46621fb 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs index a210946..fdb46dd 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs index b37dbac..15253f2 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs index 97f1463..1aeae2a 100644 --- a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs index 2188c03..1d3d867 100644 --- a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs index c7036f5..b15c281 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs index d046aa9..9d16e38 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs index fb9dbc8..8060418 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs index 121cf58..a0cc4e2 100644 --- a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs index 01e7454..2977272 100644 --- a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs index 79a0546..41b94bb 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs index e4a59c3..586d197 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs index 49c58b3..c564c40 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 0e77ba6..e6484da 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -16,7 +16,10 @@ + + + diff --git a/src/Application/Authentication/Commands/Register/RegisterCommand.cs b/src/Application/Authentication/Commands/Register/RegisterCommand.cs index 51edec4..41a1dbc 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommand.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommand.cs @@ -4,6 +4,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public record RegisterCommand : IRequest { + public string Username { get; set; } + public string Email { get; set; } public string Password { get; set; } diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs new file mode 100644 index 0000000..d3fc40f --- /dev/null +++ b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; + +public class RegisterCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(RegisterCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs index 99ca0f1..f84b3e5 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs @@ -1,21 +1,83 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public class RegisterCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly IReadOnlyCollection DefaultRoles = + new IdentityRole[] { IdentityRole.User }; - public RegisterCommandHandler(AuthenticationService authenticationService) + private readonly UnitOfWork _unitOfWork; + private readonly PasswordHasherService _passwordHasher; + + public RegisterCommandHandler(UnitOfWork unitOfWork, + PasswordHasherService passwordHasher) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _passwordHasher = passwordHasher; } - public async Task Handle( - RegisterCommand request, CancellationToken cancellationToken) + public async Task Handle(RegisterCommand request, + CancellationToken cancellationToken) { - await _authenticationService.RegisterAsync( - request.Email, request.Password, cancellationToken); + var datastoreAccount = await _unitOfWork.AccountRepository + .GetOneAsync( + e => + e.Email == request.Email || + e.Username == request.Username, + cancellationToken); + + if (datastoreAccount != null) + { + throw new RegistrationException( + "User with given email or username already registered."); + } + + + var defaultRoleIds = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => DefaultRoles.Contains(r.Value), + 1, DefaultRoles.Count, cancellationToken)) + .Items + .Select(r => r.Id); + + + var password = Encoding.UTF8.GetBytes(request.Password); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher + .HashAsync(password, salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + + var newAccount = new Account + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = defaultRoleIds.Select(id => + new AccountRole() + { + RoleId = id + }) + .ToArray() + }; + + + await _unitOfWork.AccountRepository + .AddOneAsync(newAccount, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); } } diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs index 13afc23..f009284 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs @@ -1,31 +1,54 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; +using Microsoft.Extensions.Localization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public class RegisterCommandValidator : AbstractValidator { - public RegisterCommandValidator() + public RegisterCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) { + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + RuleFor(v => v.Email) .NotEmpty() - .WithMessage("Email address is required.") - .Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b") - .WithMessage("Email address is invalid."); + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); RuleFor(v => v.Password) .NotEmpty() - .WithMessage("Password is required.") + .WithMessage(localizer["FluentValidation.NotEmpty"]) .MinimumLength(8) - .WithMessage("Password must be at least 8 characters long.") - .MaximumLength(64) - .WithMessage("Password must be at most 64 characters long.") - .Matches(@"(?=.*[A-Z]).*") - .WithMessage("Password must contain at least one uppercase letter.") - .Matches(@"(?=.*[a-z]).*") - .WithMessage("Password must contain at least one lowercase letter.") - .Matches(@"(?=.*[\d]).*") - .WithMessage("Password must contain at least one digit.") - .Matches(@"(?=.*[!@#$%^&*()]).*") - .WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*()."); + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); } } diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs index e5237e1..3d102fc 100644 --- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +// using cuqmbr.TravelGuide.Application.Common.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; @@ -7,18 +7,19 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken public class RenewAccessTokenCommandAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } + // private readonly SessionUserService _sessionUserService; + // + // public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService) + // { + // _sessionUserService = currentUserService; + // } public override void BuildPolicy(RenewAccessTokenCommand request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); + UseRequirement(new AllowAllRequirement()); + // UseRequirement(new MustBeAuthenticatedRequirement + // { + // IsAuthenticated = _sessionUserService.IsAuthenticated + // }); } } diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs index 0cb9016..b6f47c4 100644 --- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs @@ -1,22 +1,95 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; using MediatR; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; public class RenewAccessTokenCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; + private readonly JsonWebTokenConfigurationOptions _jwtConfiguration; - public RenewAccessTokenCommandHandler(AuthenticationService authenticationService) + public RenewAccessTokenCommandHandler(UnitOfWork unitOfWork, + IOptions configurationOptions) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _jwtConfiguration = configurationOptions.Value.JsonWebToken; } public async Task Handle( RenewAccessTokenCommand request, CancellationToken cancellationToken) { - return await _authenticationService.RenewAccessTokenAsync( - request.RefreshToken, cancellationToken); + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetOneAsync(e => e.Value == request.RefreshToken, + cancellationToken)); + + if (refreshToken == null) + { + throw new AuthenticationException($"Refresh token was not found."); + } + + if (!refreshToken.IsActive) + { + throw new AuthenticationException("Refresh token is inactive."); + } + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.RefreshTokens.Contains(refreshToken), + a => a.AccountRoles, cancellationToken); + + var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return new TokensModel(accessToken, refreshToken.Value); + } + + private async Task CreateJwtAsync( + Account account, CancellationToken cancellationToken) + { + var roleIds = account.AccountRoles.Select(ar => ar.RoleId); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => roleIds.Contains(r.Id), + 1, roleIds.Count(), cancellationToken)) + .Items.Select(r => r.Value); + + var roleClaims = new List(); + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role.Name)); + } + + var claims = new List() + { + new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()), + new Claim(JwtRegisteredClaimNames.Nickname, account.Username), + new Claim(JwtRegisteredClaimNames.Email, account.Email) + } + .Union(roleClaims); + + var expirationDateTimeUtc = DateTime.UtcNow.Add( + _jwtConfiguration.AccessTokenValidity); + + var symmetricSecurityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey)); + var signingCredentials = new SigningCredentials( + symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _jwtConfiguration.Issuer, + audience: _jwtConfiguration.Audience, + claims: claims, + expires: expirationDateTimeUtc, + signingCredentials: signingCredentials); + + return jwtSecurityToken; } } diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs deleted file mode 100644 index 93f2031..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public record RenewAccessTokenWithCookieCommand : IRequest { } diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs deleted file mode 100644 index 481c916..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR.Behaviors.Authorization; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandAuthorizer : - AbstractRequestAuthorizer -{ - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenWithCookieCommandAuthorizer( - SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - - public override void BuildPolicy(RenewAccessTokenWithCookieCommand request) - { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); - } -} diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs deleted file mode 100644 index 797b289..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandHandler : - IRequestHandler -{ - private readonly AuthenticationService _authenticationService; - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenWithCookieCommandHandler( - AuthenticationService authenticationService, - SessionUserService sessionUserService) - { - _authenticationService = authenticationService; - _sessionUserService = sessionUserService; - } - - public async Task Handle( - RenewAccessTokenWithCookieCommand request, - CancellationToken cancellationToken) - { - return await _authenticationService.RenewAccessTokenAsync( - _sessionUserService.RefreshToken, cancellationToken); - } -} diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs deleted file mode 100644 index a49e1ef..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentValidation; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandValidator : - AbstractValidator -{ - public RenewAccessTokenWithCookieCommandValidator() { } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs index d298795..c0fe1d9 100644 --- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs @@ -1,5 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; @@ -7,18 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok public class RevokeRefreshTokenCommandAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public RevokeRefreshTokenCommandAuthorizer(SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - public override void BuildPolicy(RevokeRefreshTokenCommand request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs index 1c0f322..2e9dee5 100644 --- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs @@ -1,4 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; using MediatR; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; @@ -6,17 +7,36 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok public class RevokeRefreshTokenCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; - public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService) + public RevokeRefreshTokenCommandHandler(UnitOfWork unitOfWork) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; } public async Task Handle( RevokeRefreshTokenCommand request, CancellationToken cancellationToken) { - await _authenticationService.RevokeRefreshTokenAsync( - request.RefreshToken, cancellationToken); + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetOneAsync(e => e.Value == request.RefreshToken, + cancellationToken)); + + if (refreshToken == null) + { + throw new AuthenticationException("Invalid refreshToken"); + } + + if (!refreshToken.IsActive) + { + throw new AuthenticationException("RefreshToken already revoked"); + } + + refreshToken.RevocationTime = DateTimeOffset.UtcNow; + + await _unitOfWork.RefreshTokenRepository + .UpdateOneAsync(refreshToken, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); } } diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs deleted file mode 100644 index f277cc8..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public record RevokeRefreshTokenWithCookieCommand : IRequest { } diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs deleted file mode 100644 index eb2f8e2..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR.Behaviors.Authorization; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandAuthorizer : - AbstractRequestAuthorizer -{ - private readonly SessionUserService _sessionUserService; - - public RevokeRefreshTokenWithCookieCommandAuthorizer( - SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - - public override void BuildPolicy(RevokeRefreshTokenWithCookieCommand request) - { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); - } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs deleted file mode 100644 index 27a8339..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandHandler : - IRequestHandler -{ - private readonly AuthenticationService _authenticationService; - private readonly SessionUserService _sessionUserService; - - public RevokeRefreshTokenWithCookieCommandHandler( - AuthenticationService authenticationService, - SessionUserService sessionUserService) - { - _authenticationService = authenticationService; - _sessionUserService = sessionUserService; - } - - public async Task Handle( - RevokeRefreshTokenWithCookieCommand request, - CancellationToken cancellationToken) - { - await _authenticationService.RevokeRefreshTokenAsync( - _sessionUserService.RefreshToken, cancellationToken); - } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs deleted file mode 100644 index c378206..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentValidation; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandValidator : - AbstractValidator -{ - public RevokeRefreshTokenWithCookieCommandValidator() { } -} diff --git a/src/Application/Authentication/Queries/Login/LoginQuery.cs b/src/Application/Authentication/Queries/Login/LoginQuery.cs index 905347c..3a2c4dd 100644 --- a/src/Application/Authentication/Queries/Login/LoginQuery.cs +++ b/src/Application/Authentication/Queries/Login/LoginQuery.cs @@ -4,7 +4,7 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public record LoginQuery : IRequest { - public string Email { get; set; } + public string EmailOrUsername { get; set; } public string Password { get; set; } } diff --git a/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs new file mode 100644 index 0000000..eb5ab38 --- /dev/null +++ b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs @@ -0,0 +1,12 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; + +public class LoginQueryAuthorizer : AbstractRequestAuthorizer +{ + public override void BuildPolicy(LoginQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs index 693a5bb..00e360b 100644 --- a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs +++ b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs @@ -1,21 +1,140 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; using MediatR; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public class LoginQueryHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; + private readonly PasswordHasherService _passwordHasher; + private readonly JsonWebTokenConfigurationOptions _jwtConfiguration; - public LoginQueryHandler(AuthenticationService authenticationService) + public LoginQueryHandler(UnitOfWork unitOfWork, + PasswordHasherService passwordHasher, + IOptions configurationOptions) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _passwordHasher = passwordHasher; + _jwtConfiguration = configurationOptions.Value.JsonWebToken; } public async Task Handle( LoginQuery request, CancellationToken cancellationToken) { - return await _authenticationService.LoginAsync( - request.Email, request.Password, cancellationToken); + var account = await _unitOfWork.AccountRepository + .GetOneAsync( + a => + a.Email == request.EmailOrUsername || + a.Username == request.EmailOrUsername, + a => a.AccountRoles, cancellationToken); + + if (account == null) + { + throw new LoginException("No users registered with given email."); + } + + var hash = Convert.FromBase64String(account.PasswordHash); + var salt = Convert.FromBase64String(account.PasswordSalt); + var password = Encoding.UTF8.GetBytes(request.Password); + + var isValidPassword = await _passwordHasher + .IsValidHashAsync(hash, password, salt, cancellationToken); + + if (!isValidPassword) + { + throw new LoginException("Given password is incorrect."); + } + + var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetPageAsync( + e => + e.AccountId == account.Id && + e.RevocationTime == null && + e.ExpirationTime > DateTimeOffset.UtcNow, + 1, int.MaxValue, cancellationToken)) + .Items.FirstOrDefault(); + + if (refreshToken == null) + { + refreshToken = CreateRefreshToken(); + refreshToken.AccountId = account.Id; + + await _unitOfWork.RefreshTokenRepository + .AddOneAsync(refreshToken, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return new TokensModel(accessToken, refreshToken.Value); + } + + private async Task CreateJwtAsync( + Account account, CancellationToken cancellationToken) + { + var roleIds = account.AccountRoles.Select(ar => ar.RoleId); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => roleIds.Contains(r.Id), + 1, roleIds.Count(), cancellationToken)) + .Items.Select(r => r.Value); + + var roleClaims = new List(); + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role.Name)); + } + + var claims = new List() + { + new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()), + new Claim(JwtRegisteredClaimNames.Nickname, account.Username), + new Claim(JwtRegisteredClaimNames.Email, account.Email) + } + .Union(roleClaims); + + var expirationDateTimeUtc = DateTime.UtcNow.Add( + _jwtConfiguration.AccessTokenValidity); + + var symmetricSecurityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey)); + var signingCredentials = new SigningCredentials( + symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _jwtConfiguration.Issuer, + audience: _jwtConfiguration.Audience, + claims: claims, + expires: expirationDateTimeUtc, + signingCredentials: signingCredentials); + + return jwtSecurityToken; + } + + private RefreshToken CreateRefreshToken() + { + var token = RandomNumberGenerator.GetBytes(128 / 8); + + return new RefreshToken + { + Guid = Guid.NewGuid(), + Value = Convert.ToBase64String(token), + CreationTime = DateTimeOffset.UtcNow, + ExpirationTime = DateTimeOffset.UtcNow.Add( + _jwtConfiguration.RefreshTokenValidity) + }; } } diff --git a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs index 9f3fb38..7a0ed0b 100644 --- a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs +++ b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs @@ -1,15 +1,18 @@ using FluentValidation; +using Microsoft.Extensions.Localization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public class LoginQueryValidator : AbstractValidator { - public LoginQueryValidator() + public LoginQueryValidator(IStringLocalizer localizer) { - RuleFor(v => v.Email) - .NotEmpty().WithMessage("Email address is required."); + RuleFor(v => v.EmailOrUsername) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); RuleFor(v => v.Password) - .NotEmpty().WithMessage("Password is required."); + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs index 60a0581..5e16639 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs index 346bd4b..3d5c276 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs index 084cfcc..f16532a 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs index 2f61edc..f5858c7 100644 --- a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs index f226338..1755ddd 100644 --- a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs index 17201fa..1b05021 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs index 2360617..64e74b8 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs index 74c6b26..4a11102 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs index 372b46e..a22724c 100644 --- a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs index b1fc747..55c1b86 100644 --- a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs index f51a43c..8c26e31 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs index 28fe46f..9d29d12 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs index 7ab1118..432f9e3 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs index 1122f5f..18d5425 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs index 51584b4..13d7708 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs index 6b20606..4dd216c 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs index ed62a53..f966a4e 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs index 700334d..c5ebf61 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs index ed1e198..263feaa 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs index 5fecbc5..8e71fb3 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs index bdc49c0..6d1ef78 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs index 84872fe..e52cd0d 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs index 3d4d568..f3b45af 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs index 53e5264..9bc82fa 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs index 55f927d..875847f 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs index 68e3aec..49827e2 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs b/src/Application/Common/Authorization/MustBeInRolesRequirement.cs index e1368d1..8035dd0 100644 --- a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs +++ b/src/Application/Common/Authorization/MustBeInRolesRequirement.cs @@ -1,6 +1,6 @@ using MediatR.Behaviors.Authorization; using Microsoft.Extensions.Localization; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.Common.Authorization; diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs index f7b1de1..0dfbce8 100644 --- a/src/Application/Common/FluentValidation/CustomValidators.cs +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -4,6 +4,14 @@ namespace cuqmbr.TravelGuide.Application.Common.FluentValidation; public static class CustomValidators { + public static IRuleBuilderOptions IsUsername( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^[a-z0-9-_.]*$"); + } + // According to RFC 5321. public static IRuleBuilderOptions IsEmail( this IRuleBuilder ruleBuilder) diff --git a/src/Application/Common/Interfaces/Services/AuthenticationService.cs b/src/Application/Common/Interfaces/Services/AuthenticationService.cs deleted file mode 100644 index 6f8932b..0000000 --- a/src/Application/Common/Interfaces/Services/AuthenticationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using cuqmbr.TravelGuide.Application.Authenticaion; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface AuthenticationService -{ - Task RegisterAsync(string email, string password, - CancellationToken cancellationToken); - - Task LoginAsync(string email, string password, - CancellationToken cancellationToken); - - Task RenewAccessTokenAsync(string refreshToken, - CancellationToken cancellationToken); - - Task RevokeRefreshTokenAsync(string refreshToken, - CancellationToken cancellationToken); -} diff --git a/src/Application/Common/Interfaces/Services/SessionUserService.cs b/src/Application/Common/Interfaces/Services/SessionUserService.cs deleted file mode 100644 index 0ffb78d..0000000 --- a/src/Application/Common/Interfaces/Services/SessionUserService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Models; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface SessionUserService -{ - public int? Id { get; } - - public Guid? Uuid { get; } - - public string? Email { get; } - - public ICollection Roles { get; } - - - public bool IsAuthenticated => Id != null; - - - public string? AccessToken { get; } - - public string? RefreshToken { get; } -} diff --git a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs index 56423e8..8f7797f 100644 --- a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs +++ b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs @@ -1,5 +1,5 @@ using AutoMapper; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; diff --git a/src/Application/Common/Persistence/Repositories/AccountRepository.cs b/src/Application/Common/Persistence/Repositories/AccountRepository.cs new file mode 100644 index 0000000..57ab40e --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AccountRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AccountRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs b/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs new file mode 100644 index 0000000..42ce1c1 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AccountRoleRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs b/src/Application/Common/Persistence/Repositories/AddressRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs rename to src/Application/Common/Persistence/Repositories/AddressRepository.cs index 3dfcbf4..0280f38 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/AddressRepository.cs +++ b/src/Application/Common/Persistence/Repositories/AddressRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface AddressRepository : BaseRepository
{ } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs b/src/Application/Common/Persistence/Repositories/AircraftRepository.cs similarity index 55% rename from src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs rename to src/Application/Common/Persistence/Repositories/AircraftRepository.cs index 8ccd4ca..3d82992 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs +++ b/src/Application/Common/Persistence/Repositories/AircraftRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface AircraftRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs b/src/Application/Common/Persistence/Repositories/BaseRepository.cs similarity index 94% rename from src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs rename to src/Application/Common/Persistence/Repositories/BaseRepository.cs index e3b78d5..a89c127 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs +++ b/src/Application/Common/Persistence/Repositories/BaseRepository.cs @@ -2,8 +2,7 @@ using System.Linq.Expressions; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface BaseRepository where TEntity : EntityBase diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs b/src/Application/Common/Persistence/Repositories/BusRepository.cs similarity index 52% rename from src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs rename to src/Application/Common/Persistence/Repositories/BusRepository.cs index 18c76dc..d06a5f3 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs +++ b/src/Application/Common/Persistence/Repositories/BusRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface BusRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs b/src/Application/Common/Persistence/Repositories/CityRepository.cs similarity index 53% rename from src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs rename to src/Application/Common/Persistence/Repositories/CityRepository.cs index beee75b..4db10a0 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs +++ b/src/Application/Common/Persistence/Repositories/CityRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface CityRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs b/src/Application/Common/Persistence/Repositories/CompanyRepository.cs similarity index 70% rename from src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs rename to src/Application/Common/Persistence/Repositories/CompanyRepository.cs index b65663a..23be434 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs +++ b/src/Application/Common/Persistence/Repositories/CompanyRepository.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces +namespace cuqmbr.TravelGuide.Application.Common .Persistence.Repositories; public interface CompanyRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs b/src/Application/Common/Persistence/Repositories/CountryRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs rename to src/Application/Common/Persistence/Repositories/CountryRepository.cs index 2be573d..1e27047 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs +++ b/src/Application/Common/Persistence/Repositories/CountryRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface CountryRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs b/src/Application/Common/Persistence/Repositories/EmployeeRepository.cs similarity index 70% rename from src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs rename to src/Application/Common/Persistence/Repositories/EmployeeRepository.cs index 012cf93..5941838 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs +++ b/src/Application/Common/Persistence/Repositories/EmployeeRepository.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces +namespace cuqmbr.TravelGuide.Application.Common .Persistence.Repositories; public interface EmployeeRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs b/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs new file mode 100644 index 0000000..b44ebec --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RefreshTokenRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs b/src/Application/Common/Persistence/Repositories/RegionRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs rename to src/Application/Common/Persistence/Repositories/RegionRepository.cs index 29ecdfe..29ba027 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs +++ b/src/Application/Common/Persistence/Repositories/RegionRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface RegionRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RoleRepository.cs b/src/Application/Common/Persistence/Repositories/RoleRepository.cs new file mode 100644 index 0000000..72d3640 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RoleRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RoleRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs b/src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs similarity index 73% rename from src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs rename to src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs index 2bac9d4..b0588ba 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs +++ b/src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces +namespace cuqmbr.TravelGuide.Application.Common .Persistence.Repositories; public interface RouteAddressDetailRepository : diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs b/src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs similarity index 57% rename from src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs rename to src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs index 4ff5733..8670b9b 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressRepository.cs +++ b/src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs @@ -1,7 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface RouteAddressRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs b/src/Application/Common/Persistence/Repositories/RouteRepository.cs similarity index 53% rename from src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs rename to src/Application/Common/Persistence/Repositories/RouteRepository.cs index 83249fb..866a0a3 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/RouteRepository.cs +++ b/src/Application/Common/Persistence/Repositories/RouteRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface RouteRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs b/src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs similarity index 71% rename from src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs rename to src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs index ead97c3..9dfe535 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs +++ b/src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces +namespace cuqmbr.TravelGuide.Application.Common .Persistence.Repositories; public interface TicketGroupRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs b/src/Application/Common/Persistence/Repositories/TicketRepository.cs similarity index 70% rename from src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs rename to src/Application/Common/Persistence/Repositories/TicketRepository.cs index 57b96aa..881008c 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs +++ b/src/Application/Common/Persistence/Repositories/TicketRepository.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces +namespace cuqmbr.TravelGuide.Application.Common .Persistence.Repositories; public interface TicketRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs b/src/Application/Common/Persistence/Repositories/TrainRepository.cs similarity index 53% rename from src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs rename to src/Application/Common/Persistence/Repositories/TrainRepository.cs index 640a507..9358316 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs +++ b/src/Application/Common/Persistence/Repositories/TrainRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface TrainRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs similarity index 62% rename from src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs rename to src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs index 6c396d2..599dd71 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs +++ b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs @@ -1,7 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface VehicleEnrollmentEmployeeRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs similarity index 59% rename from src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs rename to src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs index 1341b74..763cff0 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleEnrollmentRepository.cs +++ b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs @@ -1,7 +1,6 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface VehicleEnrollmentRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs rename to src/Application/Common/Persistence/Repositories/VehicleRepository.cs index db9fde8..7a04b65 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs +++ b/src/Application/Common/Persistence/Repositories/VehicleRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface VehicleRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Persistence/UnitOfWork.cs similarity index 73% rename from src/Application/Common/Interfaces/Persistence/UnitOfWork.cs rename to src/Application/Common/Persistence/UnitOfWork.cs index 262822a..41bd0aa 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Persistence/UnitOfWork.cs @@ -1,9 +1,11 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +namespace cuqmbr.TravelGuide.Application.Common.Persistence; public interface UnitOfWork : IDisposable { + // Application Logic + CountryRepository CountryRepository { get; } RegionRepository RegionRepository { get; } @@ -38,6 +40,16 @@ public interface UnitOfWork : IDisposable VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; } + // Identity + + AccountRepository AccountRepository { get; } + + RoleRepository RoleRepository { get; } + + AccountRoleRepository AccountRoleRepository { get; } + + RefreshTokenRepository RefreshTokenRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Common/Interfaces/Services/CurrencyConverterService.cs b/src/Application/Common/Services/CurrencyConverterService.cs similarity index 83% rename from src/Application/Common/Interfaces/Services/CurrencyConverterService.cs rename to src/Application/Common/Services/CurrencyConverterService.cs index 4f47c30..e8ae6a0 100644 --- a/src/Application/Common/Interfaces/Services/CurrencyConverterService.cs +++ b/src/Application/Common/Services/CurrencyConverterService.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Enums; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +namespace cuqmbr.TravelGuide.Application.Common.Services; public interface CurrencyConverterService { diff --git a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs b/src/Application/Common/Services/LiqPayPaymentService.cs similarity index 82% rename from src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs rename to src/Application/Common/Services/LiqPayPaymentService.cs index 768a1c3..238bb84 100644 --- a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs +++ b/src/Application/Common/Services/LiqPayPaymentService.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Enums; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +namespace cuqmbr.TravelGuide.Application.Common.Services; public interface LiqPayPaymentService { diff --git a/src/Application/Common/Services/PasswordHasherService.cs b/src/Application/Common/Services/PasswordHasherService.cs new file mode 100644 index 0000000..a3dc22c --- /dev/null +++ b/src/Application/Common/Services/PasswordHasherService.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface PasswordHasherService +{ + Task HashAsync(byte[] password, byte[] salt, + CancellationToken cancellationToken); + + Task IsValidHashAsync(byte[] hash, byte[] password, + byte[] salt, CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Interfaces/Services/SessionCurrencyService.cs b/src/Application/Common/Services/SessionCurrencyService.cs similarity index 64% rename from src/Application/Common/Interfaces/Services/SessionCurrencyService.cs rename to src/Application/Common/Services/SessionCurrencyService.cs index 258a3c0..15e7ce0 100644 --- a/src/Application/Common/Interfaces/Services/SessionCurrencyService.cs +++ b/src/Application/Common/Services/SessionCurrencyService.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Domain.Enums; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +namespace cuqmbr.TravelGuide.Application.Common.Services; public interface SessionCurrencyService { diff --git a/src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs b/src/Application/Common/Services/SessionTimeZoneService.cs similarity index 55% rename from src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs rename to src/Application/Common/Services/SessionTimeZoneService.cs index ed60d8b..4b2faaa 100644 --- a/src/Application/Common/Interfaces/Services/SessionTimeZoneService.cs +++ b/src/Application/Common/Services/SessionTimeZoneService.cs @@ -1,4 +1,4 @@ -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +namespace cuqmbr.TravelGuide.Application.Common.Services; public interface SessionTimeZoneService { diff --git a/src/Application/Common/Services/SessionUserService.cs b/src/Application/Common/Services/SessionUserService.cs new file mode 100644 index 0000000..7a868ab --- /dev/null +++ b/src/Application/Common/Services/SessionUserService.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface SessionUserService +{ + public Guid? Guid { get; } + + public string? Username { get; } + + public string? Email { get; } + + public ICollection Roles { get; } + + + public bool IsAuthenticated => Guid != null; + + + public string? AccessToken { get; } + + public string? RefreshToken { get; } +} diff --git a/src/Application/Common/Interfaces/Services/SessoionCultureService.cs b/src/Application/Common/Services/SessoionCultureService.cs similarity index 62% rename from src/Application/Common/Interfaces/Services/SessoionCultureService.cs rename to src/Application/Common/Services/SessoionCultureService.cs index 23d7389..32a1aa0 100644 --- a/src/Application/Common/Interfaces/Services/SessoionCultureService.cs +++ b/src/Application/Common/Services/SessoionCultureService.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +namespace cuqmbr.TravelGuide.Application.Common.Services; public interface SessionCultureService { diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs index 7d77ac2..ea238b8 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs index 07b4270..2605a86 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs index 209a093..5e522a4 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs index bfd16e8..446421d 100644 --- a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs index 9ceae16..da0a471 100644 --- a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs index 5f93add..a3f754b 100644 --- a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs index 78e408e..e57cda5 100644 --- a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs index a5bb800..c79afb9 100644 --- a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs index 979329c..3ed1e1a 100644 --- a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs index 49724c2..3749deb 100644 --- a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs index 9f9143c..5fb0088 100644 --- a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs index ce96fe7..f83a0b8 100644 --- a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs index 61cc0eb..a625857 100644 --- a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/ConfigurationOptions.cs b/src/Application/ConfigurationOptions.cs index e80d048..7a6edea 100644 --- a/src/Application/ConfigurationOptions.cs +++ b/src/Application/ConfigurationOptions.cs @@ -7,6 +7,8 @@ public sealed class ConfigurationOptions public LocalizationConfigurationOptions Localization { get; set; } = new(); public LoggingConfigurationOptions Logging { get; set; } = new(); + + public JsonWebTokenConfigurationOptions JsonWebToken { get; set; } = new(); } public sealed class LocalizationConfigurationOptions @@ -26,3 +28,16 @@ public sealed class LoggingConfigurationOptions public bool UseUtcTimestamp { get; set; } = true; } + +public sealed class JsonWebTokenConfigurationOptions +{ + public string Issuer { get; set; } = "localhost"; + + public string Audience { get; set; } = "localhost"; + + public string IssuerSigningKey { get; set; } = "change-me"; + + public TimeSpan AccessTokenValidity { get; set; } = TimeSpan.FromMinutes(15); + + public TimeSpan RefreshTokenValidity { get; set; } = TimeSpan.FromDays(3); +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs index 3ba50fd..930de98 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs index 91ad72c..fc7399b 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs index ec854f3..34fcdae 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs index 73b8422..1c327b1 100644 --- a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs index 8f3b55a..39fc2db 100644 --- a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs index dc4994c..dcdfbe7 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs index 7d3d822..22fe5fd 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs index 6809cb9..52e7d12 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs index 67796d8..24ac4df 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs index 2ab565d..74b3eeb 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs index ceeea99..00ac279 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs index 699f958..f158344 100644 --- a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs index 49851a9..cdd06a2 100644 --- a/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs index 8e2f0d1..1255c27 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs index 1d528b6..adb5fa0 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs index 149bc95..3a9348e 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs index e2f0478..f5b826f 100644 --- a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs index 2e7c8c3..539ebcf 100644 --- a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs index e3ff38e..52bd256 100644 --- a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs index 80d3d26..293fb38 100644 --- a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; using Microsoft.Extensions.Localization; diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs index e6cf45e..ac4fdb0 100644 --- a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs index 6011990..5334ff2 100644 --- a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs index 2e3edcb..f8f7517 100644 --- a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs index 775eeb3..b86a3cb 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs index 21a5a81..ba18ac0 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs index c85edf2..5b3e464 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Identity/Accounts/AccountDto.cs b/src/Application/Identity/Accounts/AccountDto.cs new file mode 100644 index 0000000..38dfe57 --- /dev/null +++ b/src/Application/Identity/Accounts/AccountDto.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Mappings; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts; + +public sealed class AccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public ICollection Roles { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Roles, + opt => opt.MapFrom(s => + s.AccountRoles.Select(ar => ar.Role.Value.Name))); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs new file mode 100644 index 0000000..cf5ea4f --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; + +public record AddAccountCommand : IRequest +{ + public string Email { get; set; } + + public string Password { get; set; } + + public ICollection Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs new file mode 100644 index 0000000..07ff887 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionAccountService; + + public AddAccountCommandAuthorizer(SessionUserService sessionAccountService) + { + _sessionAccountService = sessionAccountService; + } + + public override void BuildPolicy(AddAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionAccountService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionAccountService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs new file mode 100644 index 0000000..0d54557 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs @@ -0,0 +1,77 @@ +using MediatR; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Services; +using System.Security.Cryptography; +using System.Text; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasherService; + + public AddAccountCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + PasswordHasherService passwordHasherService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _passwordHasherService = passwordHasherService; + } + + public async Task Handle( + AddAccountCommand request, + CancellationToken cancellationToken) + { + var user = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (user != null) + { + throw new DuplicateEntityException(); + } + + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + e => request.Roles.Contains(e.Value), + 1, request.Roles.Count, cancellationToken)) + .Items; + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasherService.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + user = new Account() + { + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = roles.Select(r => new AccountRole() + { + RoleId = r.Id + }) + .ToArray() + }; + + user = await _unitOfWork.AccountRepository.AddOneAsync( + user, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(user); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs new file mode 100644 index 0000000..62586db --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs @@ -0,0 +1,36 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandValidator : AbstractValidator +{ + public AddAccountCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs new file mode 100644 index 0000000..c8c8bc3 --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class AddAccountViewModel +{ + public string Email { get; set; } + + public string Password { get; set; } + + public ICollection Roles { get; set; } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs new file mode 100644 index 0000000..47936af --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public record GetRolesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs new file mode 100644 index 0000000..cc167f7 --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRolesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRolesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs new file mode 100644 index 0000000..718ccd0 --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs @@ -0,0 +1,25 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryHandler : + IRequestHandler> +{ + public async Task> Handle( + GetRolesPageQuery request, + CancellationToken cancellationToken) + { + var roleStrings = IdentityRole.Enumerations.Select(e => e.Value.Name); + + var roleCount = roleStrings.Count(); + + var filteredRoles = roleStrings + .Where(r => r.Contains(request.Search)) + .ToArray(); + + return new PaginatedList( + filteredRoles, roleCount, request.PageNumber, request.PageSize); + } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs new file mode 100644 index 0000000..cbcd22b --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryValidator : AbstractValidator +{ + public GetRolesPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs index a1ad297..15e36c4 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Payments.LiqPay diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs index 94fcde6..d5ac855 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -1,8 +1,8 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using cuqmbr.TravelGuide.Application.Common.Exceptions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation.Results; using Microsoft.Extensions.Localization; diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs index 7fbc8eb..fa1f227 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs index 1f3d54b..ade9098 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -1,6 +1,6 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Application.Common.Exceptions; using System.Text; using Newtonsoft.Json; diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs index 70faa59..ccec5f5 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs index 05be471..7accab4 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs index 0b5000e..a2acfcc 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs index 9d3de32..d94ecf2 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs index 593eb3b..07f9266 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs index aba2cf7..2688c20 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs index d36cd2d..2ef62f7 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs index 5f8d461..44de767 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs index 4366378..dc726ec 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs index 55ebab2..f3fd0f3 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs index c50a2df..d7c71e2 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs index e742d58..248159e 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs index 42c1143..a14c545 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs index b064c3d..8f0ba44 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index dbec72c..832562f 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -1,10 +1,12 @@ { "FluentValidation": { - "MaximumLength": "Must less than {0:G} characters.", "NotEmpty": "Must not be empty.", "GreaterThanOrEqualTo": "Must be greater than or equal to {0}.", "LessThanOrEqualTo": "Must be less than or equal to {0}.", + "MinimumLength": "Length must greater than or equal to {0} characters.", + "MaximumLength": "Length must less than or equal to {0} characters.", "MustBeInEnum": "Must be one of the following: {0}.", + "IsUsername": "May contain lowercase latin characters (a-z), numbers (0-9), hyphens (-), underscores (_) and dots (.).", "IsEmail": "Must be a valid email address according to RFC 5321.", "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters.", "IsUnique": "Elements of the collection must be unique." @@ -34,7 +36,7 @@ "Title": "Unauthenticated access prevented.", "Detail": "Request lacks valid authentication credentials for the target resource." }, - "AithenticationException": { + "AuthenticationException": { "Title": "Authentication failed.", "Detail": "Check provided credentials validity." }, diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs index 9040482..559ff04 100644 --- a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs index df3c7cc..7b83fcf 100644 --- a/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs index 79fec33..7c33c13 100644 --- a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs index 8978db7..ec3a774 100644 --- a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs index d1fde57..bc14a6c 100644 --- a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs index 611b7ae..b4f32e6 100644 --- a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs index df48877..f4e1cea 100644 --- a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; using FluentValidation.Results; diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs index 60b8485..49314d8 100644 --- a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs index 7ca5727..16ec495 100644 --- a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs index 0f8f2db..07e1f16 100644 --- a/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; using cuqmbr.TravelGuide.Domain.Entities; diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs index dc64fa3..57e0d8e 100644 --- a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs index 15e3304..38faa54 100644 --- a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs index e38376d..945bebc 100644 --- a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs index b2c5deb..256e9a3 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs index 25d4092..d3edfc0 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -1,10 +1,10 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; using Microsoft.Extensions.Localization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation.Results; diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs index 58a8029..03c95c6 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs index 37373af..e14af8a 100644 --- a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.TicketGroups diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs index 36fdbf4..256c7ea 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs index 4644d93..b420c50 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs index 092bbc9..52e38e8 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs index df31b90..9e888b6 100644 --- a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs index 94d3ab0..b3cc70b 100644 --- a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs index c4dd607..6a5f59c 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs index e7be03f..0607955 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs index b23fb12..6a2c65e 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs index 7416f04..dc417e7 100644 --- a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs index afe796b..fe3788e 100644 --- a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs index 2a68aaa..5ce63f8 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs index 49a0c7d..39892ae 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs index a4d9410..087a2b1 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs index 19ba695..fbc4307 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs index f25ba6c..ee286ee 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -1,10 +1,10 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using QuikGraph; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs index e12f958..9ed2b6f 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs index 97eda14..055a5d4 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs index afbcc45..17318b5 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -1,11 +1,11 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using QuikGraph; using QuikGraph.Algorithms; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs index 19bb4c4..674e636 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs index dbaeae9..38f64d6 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs index b5d8d9c..dc82b55 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs index 1df07c1..415a15d 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs index ecc544b..4f9c61f 100644 --- a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs index 0dbb95b..b743321 100644 --- a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs index 9831597..ad1f9d1 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs index fb3aed7..0251fef 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; using FluentValidation.Results; diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs index 2530e8c..9e33153 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs index 58fe68f..a026fff 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs index 3b1ad69..f41760e 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs @@ -1,8 +1,8 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs index fc83c92..4d69da0 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs index 70cfc40..af32246 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs @@ -1,9 +1,9 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs index 5e0ba41..2d1d71b 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index 14b60f0..43c1312 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -48,6 +48,15 @@ "MediatR.Contracts": "2.0.1" } }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Direct", + "requested": "[9.0.5, )", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[9.0.4, )", @@ -59,6 +68,25 @@ "Microsoft.Extensions.Options": "9.0.4" } }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.11.0, )", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.11.0, )", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.3, )", @@ -182,6 +210,45 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "domain": { "type": "Project" } diff --git a/src/Configuration/Application/Configuration.cs b/src/Configuration/Application/Configuration.cs index cfbd70c..4f078cb 100644 --- a/src/Configuration/Application/Configuration.cs +++ b/src/Configuration/Application/Configuration.cs @@ -10,6 +10,8 @@ using MediatR.Behaviors.Authorization.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using cuqmbr.TravelGuide.Application.Common.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Configuration.Application; @@ -18,15 +20,16 @@ public static class Configuration public static IServiceCollection ConfigureApplication( this IServiceCollection services) { - var configurationOptions = services.BuildServiceProvider().GetService< + var configuration = services.BuildServiceProvider().GetService< IOptions>() .Value; return services - .AddLocalization(configurationOptions.Localization) + .AddLocalization(configuration.Localization) .AddFluentValidation() .AddAutoMapper() - .AddMediatR(); + .AddMediatR() + .AddAuthentication(configuration.JsonWebToken); } private static IServiceCollection AddFluentValidation( @@ -91,4 +94,42 @@ public static class Configuration .Assembly); }); } + + private static IServiceCollection AddAuthentication( + this IServiceCollection services, + JsonWebTokenConfigurationOptions configuration) + { + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = + JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.SaveToken = true; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = + new TokenValidationParameters() + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidAudience = configuration.Audience, + ValidIssuer = configuration.Issuer, + ClockSkew = TimeSpan.Zero, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes( + configuration.IssuerSigningKey)) + }; + }); + + return services; + } } diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index 5a8d825..923b7f8 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -6,8 +6,6 @@ using ApplicationConfigurationOptions = cuqmbr.TravelGuide.Application.ConfigurationOptions; using InfrastructureConfigurationOptions = cuqmbr.TravelGuide.Infrastructure.ConfigurationOptions; -using IdentityConfigurationOptions = - cuqmbr.TravelGuide.Identity.ConfigurationOptions; namespace cuqmbr.TravelGuide.Configuration.Configuration; @@ -39,10 +37,6 @@ public static class Configuration configuration.GetSection( InfrastructureConfigurationOptions.SectionName)); - services.AddOptions().Bind( - configuration.GetSection( - IdentityConfigurationOptions.SectionName)); - return services; } } diff --git a/src/Configuration/Identity/Configuration.cs b/src/Configuration/Identity/Configuration.cs deleted file mode 100644 index 0aba448..0000000 --- a/src/Configuration/Identity/Configuration.cs +++ /dev/null @@ -1,99 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.EntityFrameworkCore; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Identity.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; -using cuqmbr.TravelGuide.Identity.Exceptions; -using Microsoft.EntityFrameworkCore.Diagnostics; - -namespace cuqmbr.TravelGuide.Configuration.Identity; - -public static class Configuration -{ - public static IServiceCollection ConfigureIdentity( - this IServiceCollection services) - { - using var configurationServiceProvider = services.BuildServiceProvider(); - var configuration = configurationServiceProvider.GetService< - IOptions>() - .Value; - - // TODO: Make enum from available datastore types - - if (configuration.Datastore.Type.ToLower().Equals("postgresql")) - { - services.AddDbContext(options => - { - options.UseNpgsql( - configuration.Datastore.ConnectionString, - options => - { - options.MigrationsHistoryTable( - "ef_migrations_history", - configuration.Datastore.PartitionName); - }); - options.ConfigureWarnings(w => w.Ignore( - RelationalEventId.PendingModelChangesWarning)); - }); - - services - .AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - - if (configuration.Datastore.Migrate) - { - using var dbContextServiceProvider = services.BuildServiceProvider(); - PostgreSqlInitializer.Initialize(dbContextServiceProvider); - } - } - else - { - throw new UnSupportedDatastoreException( - $"{configuration.Datastore.Type} datastore is not supported."); - } - - services - .AddScoped(); - - services - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = - JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = - JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = - JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.IncludeErrorDetails = true; - options.SaveToken = true; - options.RequireHttpsMetadata = false; - options.TokenValidationParameters = - new TokenValidationParameters() - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidAudience = configuration.JsonWebToken.Audience, - ValidIssuer = configuration.JsonWebToken.Issuer, - ClockSkew = TimeSpan.Zero, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes( - configuration.JsonWebToken.IssuerSigningKey)) - }; - }); - - return services; - } -} diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index 3df6bb0..1c35fe6 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -1,5 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Infrastructure.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using Microsoft.Extensions.DependencyInjection; namespace cuqmbr.TravelGuide.Configuration.Infrastructure; @@ -13,11 +13,14 @@ public static class Configuration .AddHttpClient(); services + .AddScoped< + PasswordHasherService, + Pbkdf2PasswordHasherService>() .AddScoped< CurrencyConverterService, ExchangeApiCurrencyConverterService>() .AddScoped< - cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService, + cuqmbr.TravelGuide.Application.Common.Services.LiqPayPaymentService, cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>(); return services; diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index a506520..4ab16fb 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Persistence; using cuqmbr.TravelGuide.Persistence.Exceptions; using Microsoft.EntityFrameworkCore; @@ -45,10 +45,10 @@ public static class Configuration if (configuration.Migrate) { - using var dbContextServiceProvider = + using var serviceProvider = services.BuildServiceProvider(); var dbContext = - dbContextServiceProvider.GetService(); + serviceProvider.GetService(); PostgreSqlDbInitializer.Initialize(dbContext); } } @@ -79,8 +79,7 @@ public static class Configuration if (configuration.Seed) { using var serviceProvider = services.BuildServiceProvider(); - var unitOfWork = serviceProvider.GetService(); - DbSeeder.Seed(unitOfWork); + DbSeeder.Seed(serviceProvider); } return services; diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index f253c37..bba215c 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -236,8 +236,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -268,15 +268,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -359,15 +359,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -564,22 +555,13 @@ }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -651,23 +633,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -689,11 +671,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -782,6 +764,11 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -842,7 +829,10 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" @@ -851,19 +841,6 @@ "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { diff --git a/src/Domain/Entities/Account.cs b/src/Domain/Entities/Account.cs new file mode 100644 index 0000000..b477df4 --- /dev/null +++ b/src/Domain/Entities/Account.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Account : EntityBase +{ + public string Username { get; set; } + + public string Email { get; set; } + + public string PasswordHash { get; set; } + + public string PasswordSalt { get; set; } + + public ICollection AccountRoles { get; set; } + + public ICollection RefreshTokens { get; set; } +} diff --git a/src/Domain/Entities/AccountRole.cs b/src/Domain/Entities/AccountRole.cs new file mode 100644 index 0000000..fd28b0d --- /dev/null +++ b/src/Domain/Entities/AccountRole.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class AccountRole : EntityBase +{ + public long AccountId { get; set; } + + public Account Account { get; set; } + + public long RoleId { get; set; } + + public Role Role { get; set; } +} diff --git a/src/Domain/Entities/RefreshToken.cs b/src/Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..833d0e0 --- /dev/null +++ b/src/Domain/Entities/RefreshToken.cs @@ -0,0 +1,20 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class RefreshToken : EntityBase +{ + public string Value { get; set; } = null!; + + public DateTimeOffset CreationTime { get; set; } + + public DateTimeOffset ExpirationTime { get; set; } + + public DateTimeOffset? RevocationTime { get; set; } + + public bool IsExpired => DateTimeOffset.UtcNow >= ExpirationTime; + + public bool IsActive => RevocationTime == null && !IsExpired; + + public long AccountId { get; set; } + + public Account Account { get; set; } +} diff --git a/src/Domain/Entities/Role.cs b/src/Domain/Entities/Role.cs new file mode 100644 index 0000000..a0b4539 --- /dev/null +++ b/src/Domain/Entities/Role.cs @@ -0,0 +1,10 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Role : EntityBase +{ + public IdentityRole Value { get; set; } + + public ICollection AccountRoles { get; set; } +} diff --git a/src/Application/Common/Models/IdentityRole.cs b/src/Domain/Enums/IdentityRole.cs similarity index 52% rename from src/Application/Common/Models/IdentityRole.cs rename to src/Domain/Enums/IdentityRole.cs index 5636e3b..4424fb0 100644 --- a/src/Application/Common/Models/IdentityRole.cs +++ b/src/Domain/Enums/IdentityRole.cs @@ -1,11 +1,11 @@ -using cuqmbr.TravelGuide.Domain.Enums; - -namespace cuqmbr.TravelGuide.Application.Common.Models; +namespace cuqmbr.TravelGuide.Domain.Enums; public abstract class IdentityRole : Enumeration { public static readonly IdentityRole Administrator = new AdministratorRole(); public static readonly IdentityRole User = new UserRole(); + public static readonly IdentityRole CompanyOwner = new CompanyOwnerRole(); + public static readonly IdentityRole CompanyEmployee = new CompanyEmployeeRole(); protected IdentityRole(int value, string name) : base(value, name) { } @@ -18,4 +18,14 @@ public abstract class IdentityRole : Enumeration { public UserRole() : base(1, "user") { } } + + private sealed class CompanyOwnerRole : IdentityRole + { + public CompanyOwnerRole() : base(2, "company_owner") { } + } + + private sealed class CompanyEmployeeRole : IdentityRole + { + public CompanyEmployeeRole() : base(3, "company_employee") { } + } } diff --git a/src/HttpApi/Controllers/AuthenticationController.cs b/src/HttpApi/Controllers/AuthenticationController.cs index ecb2a62..6ee61b4 100644 --- a/src/HttpApi/Controllers/AuthenticationController.cs +++ b/src/HttpApi/Controllers/AuthenticationController.cs @@ -4,10 +4,6 @@ using cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; -using cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; -using cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -30,28 +26,6 @@ public class AuthenticationController : ControllerBase return await Mediator.Send(query, cancellationToken); } - // [HttpPost("loginWithCookie")] - // public async Task LoginWithCookie( - // [FromBody] LoginQuery query, - // CancellationToken cancellationToken) - // { - // var tokens = await Mediator.Send(query, cancellationToken); - // - // HttpContext.Response.Cookies.Delete("refreshToken"); - // - // var cookieOptions = new CookieOptions() - // { - // Path = "/", - // Expires = DateTimeOffset.MaxValue, - // HttpOnly = true - // }; - // - // HttpContext.Response.Cookies.Append( - // "refreshToken", tokens.RefreshToken, cookieOptions); - // - // return tokens; - // } - [HttpPost("renewAccessToken")] public async Task RenewAccessToken( [FromBody] RenewAccessTokenCommand command, @@ -60,14 +34,6 @@ public class AuthenticationController : ControllerBase return await Mediator.Send(command, cancellationToken); } - // [HttpPost("renewAccessTokenWithCookie")] - // public async Task RenewAccessTokenWithCookie( - // [FromBody] RenewAccessTokenWithCookieCommand command, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send(command, cancellationToken); - // } - [HttpPost("revokeRefreshToken")] public async Task RevokeRefreshToken( [FromBody] RevokeRefreshTokenCommand command, @@ -75,13 +41,4 @@ public class AuthenticationController : ControllerBase { await Mediator.Send(command, cancellationToken); } - - // [HttpPost("revokeRefreshTokenWithCookie")] - // public async Task RevokeRefreshTokenWithCookie( - // [FromBody] RevokeRefreshTokenWithCookieCommand command, - // CancellationToken cancellationToken) - // { - // await Mediator.Send(command, cancellationToken); - // HttpContext.Response.Cookies.Delete("refreshToken"); - // } } diff --git a/src/HttpApi/Controllers/IdentityController.cs b/src/HttpApi/Controllers/IdentityController.cs new file mode 100644 index 0000000..bacb072 --- /dev/null +++ b/src/HttpApi/Controllers/IdentityController.cs @@ -0,0 +1,242 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; +using cuqmbr.TravelGuide.Application.Identity.Accounts; +using cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +// using cuqmbr.TravelGuide.Application.Identity.Commands.AddIdentity; +// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentityPage; +// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentity; +// using cuqmbr.TravelGuide.Application.Identity.Commands.UpdateIdentity; +// using cuqmbr.TravelGuide.Application.Identity.Commands.DeleteIdentity; +// using cuqmbr.TravelGuide.Application.Identity.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("identity")] +public class IdentityController : ControllerBase +{ + [HttpGet("roles")] + [SwaggerOperation("Get a list of all roles")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetRolesPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetRolesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search + }, + cancellationToken); + } + + + + + [HttpPost("accounts")] + [SwaggerOperation("Add an account")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AccountDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddAccountViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAccountCommand() + { + Email = viewModel.Email, + Password = viewModel.Password, + Roles = viewModel.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken)); + } + + + + + // [HttpPost] + // [SwaggerOperation("Add an identity")] + // [SwaggerResponse( + // StatusCodes.Status201Created, "Object successfuly created", + // typeof(IdentityDto))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Object already exists", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Parent object not found", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task> Add( + // [FromBody] AddIdentityViewModel viewModel, + // CancellationToken cancellationToken) + // { + // return StatusCode( + // StatusCodes.Status201Created, + // await Mediator.Send( + // new AddIdentityCommand() + // { + // Name = viewModel.Name, + // Longitude = viewModel.Longitude, + // Latitude = viewModel.Latitude, + // VehicleType = VehicleType.FromName(viewModel.VehicleType), + // CityGuid = viewModel.CityUuid + // }, + // cancellationToken)); + // } + // + // [HttpGet("{uuid:guid}")] + // [SwaggerOperation("Get an identity by uuid")] + // [SwaggerResponse( + // StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Get( + // [FromRoute] Guid uuid, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send(new GetIdentityQuery() { Guid = uuid }, + // cancellationToken); + // } + // + // [HttpPut("{uuid:guid}")] + // [SwaggerOperation("Update an identity")] + // [SwaggerResponse( + // StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Object already exists", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Parent object not found", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Update( + // [FromRoute] Guid uuid, + // [FromBody] UpdateIdentityViewModel viewModel, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send( + // new UpdateIdentityCommand() + // { + // Guid = uuid, + // Name = viewModel.Name, + // Longitude = viewModel.Longitude, + // Latitude = viewModel.Latitude, + // VehicleType = VehicleType.FromName(viewModel.VehicleType), + // CityGuid = viewModel.CityUuid + // }, + // cancellationToken); + // } + // + // [HttpDelete("{uuid:guid}")] + // [SwaggerOperation("Delete an identity")] + // [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + // [SwaggerResponse( + // StatusCodes.Status400BadRequest, "Input data validation error", + // typeof(HttpValidationProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status403Forbidden, + // "Not enough privileges to perform an action", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status404NotFound, "Object not found", + // typeof(ProblemDetails))] + // [SwaggerResponse( + // StatusCodes.Status500InternalServerError, "Internal server error", + // typeof(ProblemDetails))] + // public async Task Delete( + // [FromRoute] Guid uuid, + // CancellationToken cancellationToken) + // { + // await Mediator.Send( + // new DeleteIdentityCommand() { Guid = uuid }, + // cancellationToken); + // return StatusCode(StatusCodes.Status204NoContent); + // } +} diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index be91e8b..3e2abf0 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Application.Common.Persistence; namespace cuqmbr.TravelGuide.HttpApi.Controllers; diff --git a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs index 3f245fb..3f7e4fa 100644 --- a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs +++ b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using System.Globalization; namespace cuqmbr.TravelGuide.HttpApi.Middlewares; diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index 015f880..c708d32 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -1,10 +1,9 @@ using cuqmbr.TravelGuide.Configuration.Persistence; using cuqmbr.TravelGuide.Configuration.Application; using cuqmbr.TravelGuide.Configuration.Infrastructure; -using cuqmbr.TravelGuide.Configuration.Identity; using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.HttpApi.Services; using cuqmbr.TravelGuide.HttpApi.HostedServices; using cuqmbr.TravelGuide.HttpApi.Middlewares; @@ -24,11 +23,11 @@ services.ConfigureConfiguration(args); services.ConfigureLogging(); -services.ConfigurePersistence(); -services.ConfigureIdentity(); services.ConfigureInfrastructure(); +services.ConfigurePersistence(); services.ConfigureApplication(); +services.AddHttpContextAccessor(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/HttpApi/Services/AspNetSessionCultureService.cs b/src/HttpApi/Services/AspNetSessionCultureService.cs index 1c4ef7e..7a7d614 100644 --- a/src/HttpApi/Services/AspNetSessionCultureService.cs +++ b/src/HttpApi/Services/AspNetSessionCultureService.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using System.Globalization; namespace cuqmbr.TravelGuide.HttpApi.Services; diff --git a/src/HttpApi/Services/AspNetSessionCurrencyService.cs b/src/HttpApi/Services/AspNetSessionCurrencyService.cs index 8b28990..91e4b68 100644 --- a/src/HttpApi/Services/AspNetSessionCurrencyService.cs +++ b/src/HttpApi/Services/AspNetSessionCurrencyService.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.HttpApi.Services; diff --git a/src/HttpApi/Services/AspNetSessionTimeZoneService.cs b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs index 9feb264..55acb54 100644 --- a/src/HttpApi/Services/AspNetSessionTimeZoneService.cs +++ b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; namespace cuqmbr.TravelGuide.HttpApi.Services; diff --git a/src/HttpApi/Services/AspNetSessionUserService.cs b/src/HttpApi/Services/AspNetSessionUserService.cs index eed5afc..2f9c8fd 100644 --- a/src/HttpApi/Services/AspNetSessionUserService.cs +++ b/src/HttpApi/Services/AspNetSessionUserService.cs @@ -1,6 +1,6 @@ using System.IdentityModel.Tokens.Jwt; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.HttpApi.Services; @@ -13,7 +13,7 @@ public sealed class AspNetSessionUserService : SessionUserService _httpContext = httpContextAccessor.HttpContext; } - public int? Id + public Guid? Guid { get { @@ -22,16 +22,16 @@ public sealed class AspNetSessionUserService : SessionUserService .Any(p => p.Value == JwtRegisteredClaimNames.Sub)) ?.Value; - var parsed = int.TryParse(claimValue, out var id); + var parsed = System.Guid.TryParse(claimValue, out var guid); - return parsed ? id : null; + return parsed ? guid : null; } } - public Guid? Uuid => Guid.Parse(_httpContext.User.Claims + public string? Username => _httpContext.User.Claims .FirstOrDefault(c => c.Properties - .Any(p => p.Value.Equals("uuid"))) - ?.Value); + .Any(p => p.Value == JwtRegisteredClaimNames.Nickname)) + ?.Value; public string? Email => _httpContext.User.Claims .FirstOrDefault(c => c.Properties @@ -48,6 +48,7 @@ public sealed class AspNetSessionUserService : SessionUserService _httpContext.Request.Cookies["accessToken"] ?? _httpContext.Request.Headers["Authorization"] .ToString()?.Replace("Bearer ", ""); + public string? RefreshToken => _httpContext.Request.Cookies["refreshToken"]; } diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 8f2afcb..81ff06d 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -14,6 +14,13 @@ "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, "Infrastructure": { "PaymentProcessing": { "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", @@ -24,18 +31,5 @@ } } } - }, - "Identity": { - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" - }, - "JsonWebToken": { - "Issuer": "https://api.travel-guide.cuqmbr.xyz", - "Audience": "https://travel-guide.cuqmbr.xyz", - "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", - "AccessTokenValidity": "24:00:00", - "RefreshTokenValidity": "72:00:00" - } } } diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 8f2afcb..81ff06d 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -14,6 +14,13 @@ "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, "Infrastructure": { "PaymentProcessing": { "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", @@ -24,18 +31,5 @@ } } } - }, - "Identity": { - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" - }, - "JsonWebToken": { - "Issuer": "https://api.travel-guide.cuqmbr.xyz", - "Audience": "https://travel-guide.cuqmbr.xyz", - "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", - "AccessTokenValidity": "24:00:00", - "RefreshTokenValidity": "72:00:00" - } } } diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index 2d89b37..02503f2 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -192,8 +192,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -224,15 +224,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -326,15 +326,6 @@ "Microsoft.Extensions.Identity.Core": "2.3.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -681,22 +672,13 @@ }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -793,23 +775,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -831,11 +813,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -947,6 +929,11 @@ "resolved": "7.0.0", "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, "System.Composition": { "type": "Transitive", "resolved": "7.0.0", @@ -1083,7 +1070,10 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" @@ -1096,7 +1086,6 @@ "AspNetCore.Localizer.Json": "[1.0.1, )", "Domain": "[1.0.0, )", "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", - "Identity": "[1.0.0, )", "Infrastructure": "[1.0.0, )", "Microsoft.AspNetCore.Identity": "[2.3.1, )", "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", @@ -1116,19 +1105,6 @@ "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { diff --git a/src/Identity/ConfigurationOptions.cs b/src/Identity/ConfigurationOptions.cs deleted file mode 100644 index 9bce7be..0000000 --- a/src/Identity/ConfigurationOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity; - -public sealed class ConfigurationOptions -{ - public static string SectionName { get; } = "Identity"; - - public Datastore Datastore { get; set; } = new(); - - public JsonWebToken JsonWebToken { get; set; } = new(); -} - -public sealed class Datastore -{ - public string Type { get; set; } = "inmemory"; - - public string ConnectionString { get; set; } = "InMemory"; - - public string PartitionName { get; set; } = "identity"; - - public bool Migrate { get; set; } = true; -} - -public sealed class JsonWebToken -{ - public string Issuer { get; set; } = "localhost"; - - public string Audience { get; set; } = "localhost"; - - public string IssuerSigningKey { get; set; } = "change-me"; - - public TimeSpan AccessTokenValidity { get; set; } = TimeSpan.FromMinutes(15); - - public TimeSpan RefreshTokenValidity { get; set; } = TimeSpan.FromDays(3); -} diff --git a/src/Identity/Exceptions/UnSupportedDatastoreException.cs b/src/Identity/Exceptions/UnSupportedDatastoreException.cs deleted file mode 100644 index 7366911..0000000 --- a/src/Identity/Exceptions/UnSupportedDatastoreException.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Exceptions; - -public class UnSupportedDatastoreException : Exception -{ - public UnSupportedDatastoreException() - : base() { } - - public UnSupportedDatastoreException(string message) - : base(message) { } -} - diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj deleted file mode 100644 index 53c2cb1..0000000 --- a/src/Identity/Identity.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - - - - - - true - - - diff --git a/src/Identity/IdentitySeeder.cs b/src/Identity/IdentitySeeder.cs deleted file mode 100644 index 15e7647..0000000 --- a/src/Identity/IdentitySeeder.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.Extensions.DependencyInjection; -using IdentityRoleEnum = cuqmbr.TravelGuide.Application.Common.Models - .IdentityRole; - -namespace cuqmbr.TravelGuide.Identity; - -public static class IdentitySeeder -{ - public static void Seed(IServiceProvider serviceProvider) - { - using var userManager = serviceProvider - .GetService>(); - userManager.UserValidators.Clear(); - userManager.PasswordValidators.Clear(); - - using var roleManager = serviceProvider - .GetService>(); - roleManager.RoleValidators.Clear(); - - // Seed Roles - foreach (var role in IdentityRoleEnum.Enumerations) - { - var roleString = role.Value.Name; - - var roleExists = roleManager.RoleExistsAsync(roleString).Result; - - if (roleExists) - { - continue; - } - - roleManager.CreateAsync(new IdentityRole() - { - Name = roleString, - ConcurrencyStamp = Guid.NewGuid().ToString("D") - }).Wait(); - } - - // Seed Identity - var users = new (string Email, string Password, IdentityRoleEnum[] Roles)[] - { - ("admin", "admin", new [] { IdentityRoleEnum.Administrator }), - }; - - foreach (var user in users) - { - - var userExists = userManager - .FindByEmailAsync(user.Email).Result is not null; - - if (userExists) - { - continue; - } - - var newUser = new IdentityUser - { - Email = user.Email, - NormalizedEmail = user.Email.ToUpper(), - EmailConfirmed = true, - SecurityStamp = Guid.NewGuid().ToString("D"), - RefreshTokens = default! - }; - - var hashedPassword - = userManager.PasswordHasher.HashPassword(newUser, user.Password); - newUser.PasswordHash = hashedPassword; - - userManager - .CreateAsync(newUser) - .Wait(); - - var userRoles = user.Roles.Select(x => x.Name); - - userManager - .AddToRolesAsync(newUser, userRoles) - .Wait(); - } - - } -} - diff --git a/src/Identity/Models/IdentityRole.cs b/src/Identity/Models/IdentityRole.cs deleted file mode 100644 index b1d9ee4..0000000 --- a/src/Identity/Models/IdentityRole.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class IdentityRole : Microsoft.AspNetCore.Identity.IdentityRole -{ - public string Uuid { get; set; } = Guid.NewGuid().ToString(); -} diff --git a/src/Identity/Models/IdentityUser.cs b/src/Identity/Models/IdentityUser.cs deleted file mode 100644 index ce9aad1..0000000 --- a/src/Identity/Models/IdentityUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class IdentityUser : Microsoft.AspNetCore.Identity.IdentityUser -{ - public string Uuid { get; set; } = Guid.NewGuid().ToString(); - - public ICollection RefreshTokens { get; set; } -} diff --git a/src/Identity/Models/RefreshToken.cs b/src/Identity/Models/RefreshToken.cs deleted file mode 100644 index a751096..0000000 --- a/src/Identity/Models/RefreshToken.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class RefreshToken -{ - public int Id { get; set; } - - public string Value { get; set; } = null!; - - public DateTimeOffset CreationTimestamp { get; set; } - - public DateTimeOffset ExpirationTimestamp { get; set; } - - public DateTimeOffset? RevokationTimestamp { get; set; } - - public bool IsExpired => DateTimeOffset.UtcNow >= ExpirationTimestamp; - - public bool IsActive => RevokationTimestamp is null && !IsExpired; - - public int IdentityUserId { get; set; } - - public IdentityUser IdentityUser { get; set; } -} - diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs deleted file mode 100644 index 91c8229..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityRoleClaimConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("role_claims"); - - builder - .Property(rc => rc.Id) - .HasColumnName("id"); - - builder - .Property(rc => rc.RoleId) - .HasColumnName("role_id"); - - builder - .Property(rc => rc.ClaimType) - .HasColumnName("claim_type"); - - builder - .Property(rc => rc.ClaimValue) - .HasColumnName("claim_value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs deleted file mode 100644 index e8ee518..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs +++ /dev/null @@ -1,34 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityRoleConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("roles"); - - builder - .Property(r => r.Id) - .HasColumnName("id"); - - builder - .Property(r => r.Uuid) - .HasColumnName("uuid"); - - builder - .Property(r => r.Name) - .HasColumnName("name"); - - builder - .Property(r => r.NormalizedName) - .HasColumnName("normalized_name"); - - builder - .Property(r => r.ConcurrencyStamp) - .HasColumnName("concurrency_stamp"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs deleted file mode 100644 index 863092f..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserClaimConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_claims"); - - builder - .Property(uc => uc.Id) - .HasColumnName("id"); - - builder - .Property(uc => uc.UserId) - .HasColumnName("user_id"); - - builder - .Property(uc => uc.ClaimType) - .HasColumnName("claim_type"); - - builder - .Property(uc => uc.ClaimValue) - .HasColumnName("claim_value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs deleted file mode 100644 index e99835c..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs +++ /dev/null @@ -1,128 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("users"); - - builder - .Property(u => u.Id) - .HasColumnName("id"); - - builder - .Property(u => u.Uuid) - .HasColumnName("uuid"); - - builder - .Ignore(u => u.UserName); - - builder - .Ignore(u => u.NormalizedUserName); - - builder - .Property(u => u.Email) - .HasColumnName("email"); - - builder - .Property(u => u.NormalizedEmail) - .HasColumnName("normalized_email"); - - builder - .Property(u => u.EmailConfirmed) - .HasColumnName("email_confirmed"); - - builder - .Property(u => u.PasswordHash) - .HasColumnName("password_hash"); - - builder - .Property(u => u.SecurityStamp) - .HasColumnName("security_stamp"); - - builder - .Property(u => u.ConcurrencyStamp) - .HasColumnName("concurrency_stamp"); - - builder - .Ignore(u => u.PhoneNumber); - - builder - .Ignore(u => u.PhoneNumberConfirmed); - - builder - .Property(u => u.TwoFactorEnabled) - .HasColumnName("two_factor_enabled"); - - builder - .Property(u => u.LockoutEnabled) - .HasColumnName("lockout_enabled"); - - builder - .Property(u => u.LockoutEnd) - .HasColumnName("lockout_end"); - - builder - .Property(u => u.AccessFailedCount) - .HasColumnName("access_failed_count"); - - builder - .OwnsMany(u => u.RefreshTokens, - refreshToken => - { - refreshToken - .ToTable("user_refresh_tokens"); - - refreshToken - .HasKey(rt => rt.Id) - .HasName("id"); - - refreshToken - .WithOwner(rt => rt.IdentityUser) - .HasForeignKey(rt => rt.IdentityUserId) - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - refreshToken - .Property(rt => rt.Id) - .HasColumnName("id") - .HasColumnType("int") - .IsRequired(); - - refreshToken - .Property(rt => rt.IdentityUserId) - .HasColumnName("user_id") - .HasColumnType("int") - .IsRequired(); - - refreshToken - .Property(rt => rt.Value) - .HasColumnName("value") - .HasColumnType("varchar(256)") - .IsRequired(); - - refreshToken - .Property(rt => rt.CreationTimestamp) - .HasColumnName("creation_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(); - - refreshToken - .Property(rt => rt.ExpirationTimestamp) - .HasColumnName("expiration_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(); - - refreshToken - .Property(rt => rt.RevokationTimestamp) - .HasColumnName("revokation_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(false); - } - ); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs deleted file mode 100644 index 3464a1c..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserLoginConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_logins"); - - builder - .Property(ul => ul.LoginProvider) - .HasColumnName("login_provider"); - - builder - .Property(ul => ul.ProviderKey) - .HasColumnName("provider_key"); - - builder - .Property(ul => ul.ProviderDisplayName) - .HasColumnName("provider_display_name"); - - builder - .Property(ul => ul.UserId) - .HasColumnName("user_id"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs deleted file mode 100644 index 70fee96..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserRoleConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_roles"); - - builder - .Property(ur => ur.UserId) - .HasColumnName("user_id"); - - builder - .Property(ur => ur.RoleId) - .HasColumnName("role_id"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs deleted file mode 100644 index 6b91d44..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserTokenConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_tokens"); - - builder - .Property(ut => ut.UserId) - .HasColumnName("user_id"); - - builder - .Property(ut => ut.LoginProvider) - .HasColumnName("login_provider"); - - builder - .Property(ut => ut.Name) - .HasColumnName("name"); - - builder - .Property(ut => ut.Value) - .HasColumnName("value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs deleted file mode 100644 index 5fd201a..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs +++ /dev/null @@ -1,355 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - [DbContext(typeof(PostgreSqlIdentityDbContext))] - [Migration("20250423194315_Initial_migration")] - partial class Initial_migration - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "9.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("role_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("user_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("ProviderKey") - .HasColumnType("text") - .HasColumnName("provider_key"); - - b.Property("ProviderDisplayName") - .HasColumnType("text") - .HasColumnName("provider_display_name"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("user_logins", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("user_roles", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Value") - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("user_tokens", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_name"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("roles", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessFailedCount") - .HasColumnType("integer") - .HasColumnName("access_failed_count"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean") - .HasColumnName("email_confirmed"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean") - .HasColumnName("lockout_enabled"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone") - .HasColumnName("lockout_end"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_email"); - - b.Property("PasswordHash") - .HasColumnType("text") - .HasColumnName("password_hash"); - - b.Property("SecurityStamp") - .HasColumnType("text") - .HasColumnName("security_stamp"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean") - .HasColumnName("two_factor_enabled"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.ToTable("users", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); - - b1.Property("CreationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("creation_timestamp_utc"); - - b1.Property("ExpirationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("expiration_timestamp_utc"); - - b1.Property("IdentityUserId") - .HasColumnType("int") - .HasColumnName("user_id"); - - b1.Property("RevokationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("revokation_timestamp_utc"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("varchar(256)") - .HasColumnName("value"); - - b1.HasKey("Id") - .HasName("id"); - - b1.HasIndex("IdentityUserId"); - - b1.ToTable("user_refresh_tokens", "identity"); - - b1.WithOwner("IdentityUser") - .HasForeignKey("IdentityUserId") - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - b1.Navigation("IdentityUser"); - }); - - b.Navigation("RefreshTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs deleted file mode 100644 index 035edce..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - /// - public partial class Initial_migration : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "identity"); - - migrationBuilder.CreateTable( - name: "roles", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - uuid = table.Column(type: "text", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_roles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "users", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - uuid = table.Column(type: "text", nullable: false), - email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email_confirmed = table.Column(type: "boolean", nullable: false), - password_hash = table.Column(type: "text", nullable: true), - security_stamp = table.Column(type: "text", nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true), - two_factor_enabled = table.Column(type: "boolean", nullable: false), - lockout_end = table.Column(type: "timestamp with time zone", nullable: true), - lockout_enabled = table.Column(type: "boolean", nullable: false), - access_failed_count = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "role_claims", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - role_id = table.Column(type: "integer", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_role_claims", x => x.id); - table.ForeignKey( - name: "FK_role_claims_roles_role_id", - column: x => x.role_id, - principalSchema: "identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_claims", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - user_id = table.Column(type: "integer", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user_claims", x => x.id); - table.ForeignKey( - name: "FK_user_claims_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_logins", - schema: "identity", - columns: table => new - { - login_provider = table.Column(type: "text", nullable: false), - provider_key = table.Column(type: "text", nullable: false), - provider_display_name = table.Column(type: "text", nullable: true), - user_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_logins", x => new { x.login_provider, x.provider_key }); - table.ForeignKey( - name: "FK_user_logins_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_refresh_tokens", - schema: "identity", - columns: table => new - { - id = table.Column(type: "int", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - value = table.Column(type: "varchar(256)", nullable: false), - creation_timestamp_utc = table.Column(type: "timestamptz", nullable: false), - expiration_timestamp_utc = table.Column(type: "timestamptz", nullable: false), - revokation_timestamp_utc = table.Column(type: "timestamptz", nullable: true), - user_id = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("id", x => x.id); - table.ForeignKey( - name: "fk_identityUserRefreshTokens_identityUser_userId", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_roles", - schema: "identity", - columns: table => new - { - user_id = table.Column(type: "integer", nullable: false), - role_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_roles", x => new { x.user_id, x.role_id }); - table.ForeignKey( - name: "FK_user_roles_roles_role_id", - column: x => x.role_id, - principalSchema: "identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_user_roles_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_tokens", - schema: "identity", - columns: table => new - { - user_id = table.Column(type: "integer", nullable: false), - login_provider = table.Column(type: "text", nullable: false), - name = table.Column(type: "text", nullable: false), - value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user_tokens", x => new { x.user_id, x.login_provider, x.name }); - table.ForeignKey( - name: "FK_user_tokens_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_role_claims_role_id", - schema: "identity", - table: "role_claims", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - schema: "identity", - table: "roles", - column: "normalized_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_user_claims_user_id", - schema: "identity", - table: "user_claims", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_logins_user_id", - schema: "identity", - table: "user_logins", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_refresh_tokens_user_id", - schema: "identity", - table: "user_refresh_tokens", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_roles_role_id", - schema: "identity", - table: "user_roles", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - schema: "identity", - table: "users", - column: "normalized_email"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "role_claims", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_claims", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_logins", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_refresh_tokens", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_roles", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_tokens", - schema: "identity"); - - migrationBuilder.DropTable( - name: "roles", - schema: "identity"); - - migrationBuilder.DropTable( - name: "users", - schema: "identity"); - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs b/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs deleted file mode 100644 index 6a55357..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs +++ /dev/null @@ -1,352 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - [DbContext(typeof(PostgreSqlIdentityDbContext))] - partial class PostgreSqlIdentityDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "9.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("role_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("user_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("ProviderKey") - .HasColumnType("text") - .HasColumnName("provider_key"); - - b.Property("ProviderDisplayName") - .HasColumnType("text") - .HasColumnName("provider_display_name"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("user_logins", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("user_roles", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Value") - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("user_tokens", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_name"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("roles", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessFailedCount") - .HasColumnType("integer") - .HasColumnName("access_failed_count"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean") - .HasColumnName("email_confirmed"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean") - .HasColumnName("lockout_enabled"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone") - .HasColumnName("lockout_end"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_email"); - - b.Property("PasswordHash") - .HasColumnType("text") - .HasColumnName("password_hash"); - - b.Property("SecurityStamp") - .HasColumnType("text") - .HasColumnName("security_stamp"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean") - .HasColumnName("two_factor_enabled"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.ToTable("users", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); - - b1.Property("CreationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("creation_timestamp_utc"); - - b1.Property("ExpirationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("expiration_timestamp_utc"); - - b1.Property("IdentityUserId") - .HasColumnType("int") - .HasColumnName("user_id"); - - b1.Property("RevokationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("revokation_timestamp_utc"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("varchar(256)") - .HasColumnName("value"); - - b1.HasKey("Id") - .HasName("id"); - - b1.HasIndex("IdentityUserId"); - - b1.ToTable("user_refresh_tokens", "identity"); - - b1.WithOwner("IdentityUser") - .HasForeignKey("IdentityUserId") - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - b1.Navigation("IdentityUser"); - }); - - b.Navigation("RefreshTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs deleted file mode 100644 index faca79d..0000000 --- a/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.EntityFrameworkCore; -using System.Reflection; -using Microsoft.Extensions.Options; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -public class PostgreSqlIdentityDbContext - : IdentityDbContext -{ - private readonly ConfigurationOptions _configuration; - - public PostgreSqlIdentityDbContext( - DbContextOptions options, - IOptions configurationOptions) - : base(options) - { - _configuration = configurationOptions.Value; - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - builder.HasDefaultSchema(_configuration.Datastore.PartitionName); - - builder.ApplyConfigurationsFromAssembly( - Assembly.GetExecutingAssembly(), - t => t.Namespace == - "cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations" - ); - } -} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs deleted file mode 100644 index cabb6d7..0000000 --- a/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -public static class PostgreSqlInitializer -{ - public static void Initialize(IServiceProvider serviceProvider) - { - using var dbContext = serviceProvider - .GetService(); - - var totalMigrationsCount = - dbContext.Database.GetMigrations().Count(); - var appliedMigrationCount = - dbContext.Database.GetAppliedMigrations().Count(); - - if (totalMigrationsCount - appliedMigrationCount > 0) - { - dbContext.Database.Migrate(); - } - - if (appliedMigrationCount == 0) - { - IdentitySeeder.Seed(serviceProvider); - } - } -} diff --git a/src/Identity/Services/JwtAuthenticationService.cs b/src/Identity/Services/JwtAuthenticationService.cs deleted file mode 100644 index f2a8c5d..0000000 --- a/src/Identity/Services/JwtAuthenticationService.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Security.Cryptography; -using cuqmbr.TravelGuide.Application.Authenticaion; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using cuqmbr.TravelGuide.Identity.Models; -using System.Security.Claims; -using System.Text; -using Microsoft.IdentityModel.Tokens; -using cuqmbr.TravelGuide.Application.Common.Models; -using System.IdentityModel.Tokens.Jwt; -using cuqmbr.TravelGuide.Application.Common.Exceptions; - -namespace cuqmbr.TravelGuide.Identity.Services; - -public class JwtAuthenticationService : AuthenticationService -{ - private readonly ConfigurationOptions _configuration; - - private readonly UserManager _userManager; - private readonly RoleManager _roleManager; - - public JwtAuthenticationService( - UserManager userManager, - RoleManager roleManager, - IOptions configuration) - { - _userManager = userManager; - _userManager.UserValidators.Clear(); - _userManager.PasswordValidators.Clear(); - - _roleManager = roleManager; - - _configuration = configuration.Value; - } - - public async Task RegisterAsync( - string email, - string password, - CancellationToken cancellationToken) - { - var userWithSameEmail = await _userManager.FindByEmailAsync(email); - if (userWithSameEmail is not null) - { - throw new RegistrationException("User with given email already registered."); - } - - var newUser = new IdentityUser - { - // Id = Guid.NewGuid().ToString(), - Email = email, - RefreshTokens = default! - }; - - var createUserResult = await _userManager.CreateAsync(newUser, password); - - if (createUserResult.Errors.Any()) - { - var errorMessage = String.Join("\n", - createUserResult.Errors.Select(e => e.Description)); - throw new AuthenticationException(errorMessage); - } - - var addToRoleResult = - await _userManager.AddToRoleAsync(newUser, Application.Common.Models.IdentityRole.User.Name); - - if (addToRoleResult.Errors.Any()) - { - var errorMessage = String.Join("\n", - addToRoleResult.Errors.Select(e => e.Description)); - throw new AuthenticationException(errorMessage); - } - } - - public async Task LoginAsync( - string email, - string password, - CancellationToken cancellationToken) - { - var user = await _userManager.FindByEmailAsync(email); - if (user is null) - { - throw new LoginException("No users registered with given email."); - } - - var isPasswordCorrect = await _userManager.CheckPasswordAsync(user, password); - if (!isPasswordCorrect) - { - throw new LoginException("Given password is incorrect."); - } - - var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); - var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - - var refreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive); - if (refreshToken is null) - { - refreshToken = CreateRefreshToken(); - refreshToken.IdentityUserId = user.Id; - user.RefreshTokens.Add(refreshToken); - var result = await _userManager.UpdateAsync(user); - } - - return new TokensModel(accessToken, refreshToken.Value); - } - - public async Task RenewAccessTokenAsync( - string refreshToken, - CancellationToken cancellationToken) - { - var user = _userManager.Users.SingleOrDefault(u => - u.RefreshTokens.Any(rt => rt.Value == refreshToken)); - if (user is null) - { - throw new AuthenticationException($"Refresh token was not found."); - } - - var refreshTokenObject = user.RefreshTokens.Single(rt => - rt.Value == refreshToken); - if (!refreshTokenObject.IsActive) - { - throw new AuthenticationException("Refresh token is inactive."); - } - - var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); - var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - - return new TokensModel(accessToken, refreshToken); - } - - public async Task RevokeRefreshTokenAsync( - string refreshToken, - CancellationToken cancellationToken) - { - var user = _userManager.Users.SingleOrDefault(u => - u.RefreshTokens.Any(t => t.Value == refreshToken)); - if (user is null) - { - throw new AuthenticationException("Invalid refreshToken"); - } - - var refreshTokenObject = user.RefreshTokens.Single(x => - x.Value == refreshToken); - if (!refreshTokenObject.IsActive) - { - throw new AuthenticationException("RefreshToken already revoked"); - } - - refreshTokenObject.RevokationTimestamp = DateTimeOffset.UtcNow; - await _userManager.UpdateAsync(user); - } - - private async Task CreateJwtAsync( - IdentityUser user, - CancellationToken cancellationToken) - { - var userClaims = await _userManager.GetClaimsAsync(user); - - var roles = await _userManager.GetRolesAsync(user); - var roleClaims = new List(); - foreach (var role in roles) - { - roleClaims.Add(new Claim("roles", role)); - } - - var claims = new List() - { - new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new Claim(JwtRegisteredClaimNames.Email, user.Email) - } - .Union(userClaims) - .Union(roleClaims); - - var expirationDateTimeUtc = DateTime.UtcNow.Add( - _configuration.JsonWebToken.AccessTokenValidity); - - var symmetricSecurityKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(_configuration.JsonWebToken.IssuerSigningKey)); - var signingCredentials = new SigningCredentials( - symmetricSecurityKey, SecurityAlgorithms.HmacSha256); - - var jwtSecurityToken = new JwtSecurityToken( - issuer: _configuration.JsonWebToken.Issuer, - audience: _configuration.JsonWebToken.Audience, - claims: claims, - expires: expirationDateTimeUtc, - signingCredentials: signingCredentials); - - return jwtSecurityToken; - } - - private RefreshToken CreateRefreshToken() - { - var randomNumber = new byte[32]; - - using var rng = RandomNumberGenerator.Create(); - rng.GetNonZeroBytes(randomNumber); - - return new RefreshToken - { - // Id = Guid.NewGuid().ToString(), - Value = Convert.ToBase64String(randomNumber), - CreationTimestamp = DateTimeOffset.UtcNow, - ExpirationTimestamp = DateTimeOffset.UtcNow.Add( - _configuration.JsonWebToken.RefreshTokenValidity) - }; - } -} diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json deleted file mode 100644 index 5c6200a..0000000 --- a/src/Identity/packages.lock.json +++ /dev/null @@ -1,612 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Microsoft.AspNetCore.Authentication.JwtBearer": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", - "dependencies": { - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" - } - }, - "Microsoft.AspNetCore.Identity": { - "type": "Direct", - "requested": "[2.3.1, )", - "resolved": "2.3.1", - "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", - "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", - "Microsoft.Extensions.Identity.Core": "2.3.0" - } - }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, - "Microsoft.Extensions.Options": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Direct", - "requested": "[8.8.0, )", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Direct", - "requested": "[8.8.0, )", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "AspNetCore.Localizer.Json": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", - "dependencies": { - "Microsoft.AspNetCore.Components": "9.0.0", - "Microsoft.Extensions.Caching.Memory": "9.0.0", - "Microsoft.Extensions.Localization": "9.0.0" - } - }, - "AutoMapper": { - "type": "Transitive", - "resolved": "14.0.0", - "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", - "dependencies": { - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "FluentValidation": { - "type": "Transitive", - "resolved": "11.11.0", - "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" - }, - "MediatR": { - "type": "Transitive", - "resolved": "12.4.1", - "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", - "dependencies": { - "MediatR.Contracts": "[2.0.1, 3.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "MediatR.Behaviors.Authorization": { - "type": "Transitive", - "resolved": "12.2.0", - "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", - "dependencies": { - "MediatR": "12.4.1", - "MediatR.Contracts": "2.0.1" - } - }, - "MediatR.Contracts": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" - }, - "Microsoft.AspNetCore.Authentication": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Core": "2.3.0", - "Microsoft.AspNetCore.DataProtection": "2.3.0", - "Microsoft.AspNetCore.Http": "2.3.0", - "Microsoft.AspNetCore.Http.Extensions": "2.3.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Extensions.WebEncoders": "8.0.11" - } - }, - "Microsoft.AspNetCore.Authentication.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.AspNetCore.Authentication.Cookies": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", - "dependencies": { - "Microsoft.AspNetCore.Authentication": "2.3.0" - } - }, - "Microsoft.AspNetCore.Authentication.Core": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Http": "2.3.0", - "Microsoft.AspNetCore.Http.Extensions": "2.3.0" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.AspNetCore.Components": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "9.0.0", - "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" - } - }, - "Microsoft.AspNetCore.Components.Analyzers": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" - } - }, - "Microsoft.AspNetCore.DataProtection": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", - "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Win32.Registry": "4.5.0", - "System.Security.Cryptography.Xml": "8.0.2", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.AspNetCore.DataProtection.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" - }, - "Microsoft.AspNetCore.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" - } - }, - "Microsoft.AspNetCore.Hosting.Server.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.3.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - } - }, - "Microsoft.AspNetCore.Http": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.AspNetCore.WebUtilities": "2.3.0", - "Microsoft.Extensions.ObjectPool": "8.0.11", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Net.Http.Headers": "2.3.0" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.3.0", - "System.Text.Encodings.Web": "8.0.0" - } - }, - "Microsoft.AspNetCore.Http.Extensions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Net.Http.Headers": "2.3.0", - "System.Buffers": "4.6.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" - }, - "Microsoft.AspNetCore.WebUtilities": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", - "dependencies": { - "Microsoft.Net.Http.Headers": "2.3.0", - "System.Text.Encodings.Web": "8.0.0" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", - "Microsoft.Extensions.Caching.Memory": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.4", - "Microsoft.Extensions.Caching.Memory": "9.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", - "Microsoft.Extensions.Logging.Abstractions": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4", - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.Extensions.Localization": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Localization.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.Extensions.Localization.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.4", - "Microsoft.Extensions.Logging.Abstractions": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.11", - "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" - }, - "Microsoft.Extensions.WebEncoders": { - "type": "Transitive", - "resolved": "8.0.11", - "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "8.0.1", - "System.IdentityModel.Tokens.Jwt": "8.0.1" - } - }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0", - "System.Buffers": "4.6.0" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "QuikGraph": { - "type": "Transitive", - "resolved": "2.5.0", - "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "System.Linq.Dynamic.Core": { - "type": "Transitive", - "resolved": "1.6.2", - "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "8.0.1" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, - "application": { - "type": "Project", - "dependencies": { - "AspNetCore.Localizer.Json": "[1.0.1, )", - "AutoMapper": "[14.0.0, )", - "Domain": "[1.0.0, )", - "FluentValidation": "[11.11.0, )", - "MediatR": "[12.4.1, )", - "MediatR.Behaviors.Authorization": "[12.2.0, )", - "Microsoft.Extensions.Logging": "[9.0.4, )", - "Newtonsoft.Json": "[13.0.3, )", - "QuikGraph": "[2.5.0, )", - "System.Linq.Dynamic.Core": "[1.6.2, )" - } - }, - "domain": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs index e2bd323..7c36d53 100644 --- a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -1,5 +1,5 @@ using System.Globalization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using Newtonsoft.Json; diff --git a/src/Infrastructure/Services/LiqPayPaymentService.cs b/src/Infrastructure/Services/LiqPayPaymentService.cs index 70fc88b..dce188b 100644 --- a/src/Infrastructure/Services/LiqPayPaymentService.cs +++ b/src/Infrastructure/Services/LiqPayPaymentService.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; namespace cuqmbr.TravelGuide.Infrastructure.Services; public sealed class LiqPayPaymentService : - cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService + cuqmbr.TravelGuide.Application.Common.Services.LiqPayPaymentService { private readonly LiqPayConfigurationOptions _configuration; private readonly string _callbackAddressBase; diff --git a/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs b/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs new file mode 100644 index 0000000..2f4e5bf --- /dev/null +++ b/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using cuqmbr.TravelGuide.Application.Common.Services; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class Pbkdf2PasswordHasherService : PasswordHasherService +{ + private const int IterationCount = 210_000; + + private readonly HashAlgorithmName Hash = HashAlgorithmName.SHA3_512; + private const byte HashLengthBytes = 64; + + public Task HashAsync(byte[] password, byte[] salt, + CancellationToken cancellationToken) + { + return Task.FromResult( + Rfc2898DeriveBytes.Pbkdf2( + password, salt, IterationCount, + Hash, HashLengthBytes)); + } + + public Task IsValidHashAsync(byte[] hash, byte[] password, + byte[] salt, CancellationToken cancellationToken) + { + var computedHash = Rfc2898DeriveBytes.Pbkdf2( + password, salt, IterationCount, + Hash, HashLengthBytes); + + return Task.FromResult( + Enumerable.SequenceEqual(computedHash, hash)); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index b07e4dd..47ecef3 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -68,6 +68,14 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "9.0.0", @@ -234,11 +242,67 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, "QuikGraph": { "type": "Transitive", "resolved": "2.5.0", "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "System.Linq.Dynamic.Core": { "type": "Transitive", "resolved": "1.6.2", @@ -253,7 +317,10 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" diff --git a/src/Persistence/DbSeeder.cs b/src/Persistence/DbSeeder.cs index baefefe..229bc7d 100644 --- a/src/Persistence/DbSeeder.cs +++ b/src/Persistence/DbSeeder.cs @@ -1,12 +1,105 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.DependencyInjection; namespace cuqmbr.TravelGuide.Persistence; public static class DbSeeder { - public static void Seed(UnitOfWork unitOfWork) + public static void Seed(IServiceProvider serviceProvider) { - // Do Seeding Here + var unitOfWork = + serviceProvider.GetRequiredService(); + + var passwordHasher = + serviceProvider.GetRequiredService(); + + + // Seed Roles + { + var datastoreRoles = unitOfWork.RoleRepository + .GetPageAsync(1, IdentityRole.Enumerations.Count, + CancellationToken.None) + .Result.Items.Select(r => r.Value); + + var roles = IdentityRole.Enumerations.Select(r => r.Value); + + foreach (var role in roles) + { + + if (datastoreRoles.Contains(role)) + { + continue; + } + + unitOfWork.RoleRepository.AddOneAsync( + new Role() { Value = role }, + CancellationToken.None).Wait(); + } + + unitOfWork.SaveAsync(CancellationToken.None).Wait(); + } + + // Seed Accounts + { + var accounts = + new (string Username, string Email, + string Password, IdentityRole[] Roles)[] + { + ("admin", "admin", "admin", + new [] { IdentityRole.Administrator }), + }; + + var roles = unitOfWork.RoleRepository + .GetPageAsync(1, IdentityRole.Enumerations.Count, + CancellationToken.None) + .Result.Items; + + foreach (var account in accounts) + { + var datastoreAccount = + unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == account.Email, CancellationToken.None) + .Result; + + if (datastoreAccount != null) + { + continue; + } + + var password = Encoding.UTF8.GetBytes(account.Password); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = passwordHasher + .HashAsync(password, salt, CancellationToken.None) + .Result; + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + unitOfWork.AccountRepository.AddOneAsync( + new Account() + { + Username = account.Username, + Email = account.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = account.Roles.Select(ar => + new AccountRole() + { + RoleId = roles.Single(dr => dr.Value.Equals(ar)).Id + }) + .ToArray() + }, + CancellationToken.None).Wait(); + } + + unitOfWork.SaveAsync(CancellationToken.None).Wait(); + } unitOfWork.Dispose(); } diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 41f3171..2d41303 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -55,5 +55,11 @@ public class InMemoryDbContext : DbContext builder .Properties() .HaveConversion(); + + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); } } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index c04be86..ee832c6 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -1,5 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Persistence.InMemory.Repositories; namespace cuqmbr.TravelGuide.Persistence.InMemory; @@ -34,6 +34,11 @@ public sealed class InMemoryUnitOfWork : UnitOfWork new InMemoryRouteAddressDetailRepository(_dbContext); VehicleEnrollmentEmployeeRepository = new InMemoryVehicleEnrollmentEmployeeRepository(_dbContext); + + AccountRepository = new InMemoryAccountRepository(_dbContext); + RoleRepository = new InMemoryRoleRepository(_dbContext); + AccountRoleRepository = new InMemoryAccountRoleRepository(_dbContext); + RefreshTokenRepository = new InMemoryRefreshTokenRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -70,6 +75,15 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + + public AccountRepository AccountRepository { get; init; } + + public RoleRepository RoleRepository { get; init; } + + public AccountRoleRepository AccountRoleRepository { get; init; } + + public RefreshTokenRepository RefreshTokenRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs new file mode 100644 index 0000000..aa24b2e --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAccountRepository : + InMemoryBaseRepository, AccountRepository +{ + public InMemoryAccountRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs new file mode 100644 index 0000000..eed11a7 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAccountRoleRepository : + InMemoryBaseRepository, AccountRoleRepository +{ + public InMemoryAccountRoleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs index 3b206f1..95982ab 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs index 86373d5..cb203b6 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs index 8e628a8..609ab72 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; diff --git a/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs index 2180258..6b40e7b 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs index 22f942a..b6aca53 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs index 66b2516..8d65211 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs index a0e0e1f..bc9812f 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs index 40e9b63..939f74e 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs new file mode 100644 index 0000000..dff079b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRefreshTokenRepository : + InMemoryBaseRepository, RefreshTokenRepository +{ + public InMemoryRefreshTokenRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs index 63a668e..5d7e7ee 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs new file mode 100644 index 0000000..81bbbc4 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRoleRepository : + InMemoryBaseRepository, RoleRepository +{ + public InMemoryRoleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs index 992e9a1..9a7ee0b 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs index ce24624..70e6141 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs index f728978..27bb20f 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs index 613715b..b160761 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs index ab406b0..398bbab 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs index 603b223..971391c 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs index d9b8d21..3705f86 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs index 2bf6313..520d174 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs index a0bd558..ae36b4b 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs new file mode 100644 index 0000000..dc16077 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs @@ -0,0 +1,48 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AccountConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("accounts"); + + base.Configure(builder); + + + builder + .Property(a => a.Username) + .HasColumnName("username") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(a => a.Email) + .HasColumnName("email") + .HasColumnType("varchar(256)") + .IsRequired(true); + + // Base64 encoded PBKDF2 SHA512. + // Base64 represents 3 input bytes 4 output bytes, it adds padding + // if the input bytes count isn't divisible by 3. + // 512 / 8 / 3 = 21.(3) => 22 input characters (with padding) => + // 88 output characters. + builder + .Property(a => a.PasswordHash) + .HasColumnName("password_hash") + .HasColumnType("varchar(88)") + .IsRequired(true); + + // Base64 encoded 128 bits + // 128 / 8 / 3 = 5.(3) => 6 in chars \w padding => 24 out chars + builder + .Property(a => a.PasswordSalt) + .HasColumnName("password_salt") + .HasColumnType("varchar(24)") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs new file mode 100644 index 0000000..50ae6aa --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs @@ -0,0 +1,64 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AccountRoleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("account_roles"); + + base.Configure(builder); + + + builder + .Property(ar => ar.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ar => ar.Account) + .WithMany(a => a.AccountRoles) + .HasForeignKey(ar => ar.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ar => ar.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.AccountId).Metadata.GetColumnName()}"); + + + builder + .Property(ar => ar.RoleId) + .HasColumnName("role_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ar => ar.Role) + .WithMany(r => r.AccountRoles) + .HasForeignKey(ar => ar.RoleId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.RoleId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ar => ar.RoleId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.RoleId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs index c7a8bac..f9b0128 100644 --- a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -9,8 +9,6 @@ public class BaseConfiguration : IEntityTypeConfiguration { public virtual void Configure(EntityTypeBuilder builder) { - // Set table name for inherited types using type name - // instead of mapped table name var tableName = builder.Metadata.GetTableName(); builder diff --git a/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs new file mode 100644 index 0000000..5265083 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs @@ -0,0 +1,67 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RefreshTokenConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("refresh_tokens"); + + base.Configure(builder); + + + // Base64 encoded 128 bits + // 128 / 8 / 3 = 5.(3) => 6 in chars \w padding => 24 out chars + builder + .Property(rt => rt.Value) + .HasColumnName("value") + .HasColumnType("varchar(24)") + .IsRequired(true); + + builder + .Property(rt => rt.CreationTime) + .HasColumnName("creation_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(rt => rt.ExpirationTime) + .HasColumnName("expiration_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(rt => rt.RevocationTime) + .HasColumnName("revocation_time") + .HasColumnType("timestamptz") + .IsRequired(false); + + + builder + .Property(rt => rt.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(rt => rt.Account) + .WithMany(rt => rt.RefreshTokens) + .HasForeignKey(rt => rt.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rt => rt.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rt => rt.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rt => rt.AccountId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs new file mode 100644 index 0000000..90a3e21 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RoleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(r => r.Value) + .HasColumnName("name") + .IsRequired(true); + + builder + .ToTable( + "roles", + r => r.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.Value) + .Metadata.GetColumnName()}", + $"{builder.Property(r => r.Value) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", IdentityRole.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs new file mode 100644 index 0000000..44068d3 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs @@ -0,0 +1,1294 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250528083243_Add_Account_Role_and_AccountRole")] + partial class Add_Account_Role_and_AccountRole + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs new file mode 100644 index 0000000..0cdd345 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs @@ -0,0 +1,175 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Account_Role_and_AccountRole : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "account_roles_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "accounts_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "refresh_tokens_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "roles_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "accounts", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.accounts_id_sequence')"), + username = table.Column(type: "varchar(32)", nullable: false), + email = table.Column(type: "varchar(256)", nullable: false), + password_hash = table.Column(type: "varchar(88)", nullable: false), + password_salt = table.Column(type: "varchar(24)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_accounts", x => x.id); + table.UniqueConstraint("altk_accounts_uuid", x => x.uuid); + }); + + migrationBuilder.CreateTable( + name: "roles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.roles_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_roles", x => x.id); + table.UniqueConstraint("altk_roles_uuid", x => x.uuid); + table.CheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + + migrationBuilder.CreateTable( + name: "refresh_tokens", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.refresh_tokens_id_sequence')"), + value = table.Column(type: "varchar(24)", nullable: false), + creation_time = table.Column(type: "timestamptz", nullable: false), + expiration_time = table.Column(type: "timestamptz", nullable: false), + revocation_time = table.Column(type: "timestamptz", nullable: true), + account_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_refresh_tokens", x => x.id); + table.UniqueConstraint("altk_refresh_tokens_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_refresh_tokens_account_id", + column: x => x.account_id, + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_roles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.account_roles_id_sequence')"), + account_id = table.Column(type: "bigint", nullable: false), + role_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_account_roles", x => x.id); + table.UniqueConstraint("altk_account_roles_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_account_roles_account_id", + column: x => x.account_id, + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_account_roles_role_id", + column: x => x.role_id, + principalSchema: "application", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_account_roles_account_id", + schema: "application", + table: "account_roles", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_roles_role_id", + schema: "application", + table: "account_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "ix_refresh_tokens_account_id", + schema: "application", + table: "refresh_tokens", + column: "account_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "account_roles", + schema: "application"); + + migrationBuilder.DropTable( + name: "refresh_tokens", + schema: "application"); + + migrationBuilder.DropTable( + name: "roles", + schema: "application"); + + migrationBuilder.DropTable( + name: "accounts", + schema: "application"); + + migrationBuilder.DropSequence( + name: "account_roles_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "accounts_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "refresh_tokens_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "roles_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 1f16747..ba23151 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -23,6 +23,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + modelBuilder.HasSequence("addresses_id_sequence"); modelBuilder.HasSequence("cities_id_sequence"); @@ -35,8 +39,12 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("employees_id_sequence"); + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + modelBuilder.HasSequence("regions_id_sequence"); + modelBuilder.HasSequence("roles_id_sequence"); + modelBuilder.HasSequence("route_address_details_id_sequence"); modelBuilder.HasSequence("route_addresses_id_sequence"); @@ -53,6 +61,86 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("vehicles_id_sequence"); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") @@ -308,6 +396,53 @@ namespace Persistence.PostgreSql.Migrations }); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Property("Id") @@ -343,6 +478,37 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("regions", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => { b.Property("Id") @@ -817,6 +983,27 @@ namespace Persistence.PostgreSql.Migrations b.HasDiscriminator().HasValue("train"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") @@ -865,6 +1052,18 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Employee"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") @@ -1010,6 +1209,13 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("VehicleEnrollment"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("RefreshTokens"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Navigation("AddressRoutes"); @@ -1044,6 +1250,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Cities"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => { b.Navigation("RouteAddresses"); diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 0f5571c..4825aac 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -62,5 +62,11 @@ public class PostgreSqlDbContext : DbContext builder .Properties() .HaveConversion(); + + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); } } diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 717d684..5b42993 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -1,5 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; namespace cuqmbr.TravelGuide.Persistence.PostgreSql; @@ -34,6 +34,11 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork new PostgreSqlRouteAddressDetailRepository(_dbContext); VehicleEnrollmentEmployeeRepository = new PostgreSqlVehicleEnrollmentEmployeeRepository(_dbContext); + + AccountRepository = new PostgreSqlAccountRepository(_dbContext); + RoleRepository = new PostgreSqlRoleRepository(_dbContext); + AccountRoleRepository = new PostgreSqlAccountRoleRepository(_dbContext); + RefreshTokenRepository = new PostgreSqlRefreshTokenRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -70,6 +75,15 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + + public AccountRepository AccountRepository { get; init; } + + public RoleRepository RoleRepository { get; init; } + + public AccountRoleRepository AccountRoleRepository { get; init; } + + public RefreshTokenRepository RefreshTokenRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs new file mode 100644 index 0000000..e622829 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAccountRepository : + PostgreSqlBaseRepository, AccountRepository +{ + public PostgreSqlAccountRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs new file mode 100644 index 0000000..6a683dc --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAccountRoleRepository : + PostgreSqlBaseRepository, AccountRoleRepository +{ + public PostgreSqlAccountRoleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs index 58915b0..f50216b 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs index 292fcea..d6cb3d8 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs index cc3930a..468fb49 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs index da4c844..7f9c09c 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs index 441d6b9..0fdb212 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs index ed857cd..c0862bc 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs index 78d315b..b45a481 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs index 03319c3..36d1176 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs new file mode 100644 index 0000000..6b436ed --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRefreshTokenRepository : + PostgreSqlBaseRepository, RefreshTokenRepository +{ + public PostgreSqlRefreshTokenRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs index 036018e..8be382d 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs new file mode 100644 index 0000000..e549ba2 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRoleRepository : + PostgreSqlBaseRepository, RoleRepository +{ + public PostgreSqlRoleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs index d88305f..11b9c48 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs index fcd828b..9f8d7e7 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs index 1af92ff..c30b052 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs index 204d808..42af631 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs index b2f53f6..96a9bf6 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs index 2dffccc..0bab7ce 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs index ed6ca66..1a11fba 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs index 434b69c..6ae4d01 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs index da7cf91..536af9c 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/TypeConverters/RoleConverter.cs b/src/Persistence/TypeConverters/RoleConverter.cs new file mode 100644 index 0000000..d809f43 --- /dev/null +++ b/src/Persistence/TypeConverters/RoleConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class RoleConverter : ValueConverter +{ + public RoleConverter() + : base( + v => v.Name, + v => IdentityRole.FromName(v)) + { } +} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index 4833b36..bce5250 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -108,6 +108,14 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "9.0.0", @@ -266,6 +274,53 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -314,6 +369,15 @@ "SQLitePCLRaw.core": "2.1.10" } }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "System.Linq.Dynamic.Core": { "type": "Transitive", "resolved": "1.6.2", @@ -338,7 +402,10 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" diff --git a/tst/Application.IntegrationTests/BaseTest.cs b/tst/Application.IntegrationTests/BaseTest.cs index 194ab99..26ebcb3 100644 --- a/tst/Application.IntegrationTests/BaseTest.cs +++ b/tst/Application.IntegrationTests/BaseTest.cs @@ -3,12 +3,11 @@ using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; using cuqmbr.TravelGuide.Configuration.Application; using cuqmbr.TravelGuide.Configuration.Persistence; -using cuqmbr.TravelGuide.Configuration.Identity; using Moq; using System.Globalization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.IntegrationTests; @@ -61,8 +60,8 @@ public abstract class TestBase : IDisposable var guid = Guid.NewGuid(); mock.Setup(s => s.Email).Returns(guid.ToString()); - mock.Setup(s => s.Id).Returns(guid.GetHashCode()); - mock.Setup(s => s.Uuid).Returns(Guid.NewGuid()); + mock.Setup(s => s.Guid).Returns(guid); + mock.Setup(s => s.Guid).Returns(Guid.NewGuid()); mock.Setup(s => s.IsAuthenticated).Returns(true); mock.Setup(s => s.Roles).Returns(roles); diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs index 5a7b514..438e308 100644 --- a/tst/Application.IntegrationTests/CitiesTests.cs +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; @@ -9,7 +9,6 @@ using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; - namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class CitiesTests : TestBase diff --git a/tst/Application.IntegrationTests/CountriesTests.cs b/tst/Application.IntegrationTests/CountriesTests.cs index 43b5f88..72d6af3 100644 --- a/tst/Application.IntegrationTests/CountriesTests.cs +++ b/tst/Application.IntegrationTests/CountriesTests.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; @@ -7,7 +7,6 @@ using cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; - namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class CountriesTests : TestBase diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 7dbc6e8..372300e 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; @@ -8,7 +8,6 @@ using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; - namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class RegionsTests : TestBase diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 117f2e7..1a02d20 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -154,8 +154,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -186,15 +186,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -288,15 +288,6 @@ "Microsoft.Extensions.Identity.Core": "2.3.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -566,22 +557,13 @@ }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -687,23 +669,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -725,11 +707,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -856,6 +838,11 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -986,7 +973,10 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" @@ -999,7 +989,6 @@ "AspNetCore.Localizer.Json": "[1.0.1, )", "Domain": "[1.0.0, )", "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", - "Identity": "[1.0.0, )", "Infrastructure": "[1.0.0, )", "Microsoft.AspNetCore.Identity": "[2.3.1, )", "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", @@ -1019,19 +1008,6 @@ "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { From 2d7d23d26b1f7a2d9668a17282bbf7b551f16d0c Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 28 May 2025 12:35:04 +0300 Subject: [PATCH 25/35] update ci pipeline to reflect Identity project removeal --- .github/workflows/ci-cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 56207bd..21dede3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -25,7 +25,6 @@ jobs: cache: true cache-dependency-path: | src/Application/packages.lock.json - src/Identity/packages.lock.json src/Infrastructure/packages.lock.json src/Persistence/packages.lock.json src/Configuration/packages.lock.json From 7229a10ad559b3619407ae4e005af72ee3671833 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 28 May 2025 15:40:30 +0300 Subject: [PATCH 26/35] add account management --- .../Commands/AddAccount/AddAccountCommand.cs | 5 +- .../AddAccount/AddAccountCommandAuthorizer.cs | 3 +- .../AddAccount/AddAccountCommandHandler.cs | 28 +- .../AddAccount/AddAccountCommandValidator.cs | 38 ++- .../DeleteAccount/DeleteAccountCommand.cs | 9 + .../DeleteAccountCommandAuthorizer.cs | 32 ++ .../DeleteAccountCommandHandler.cs | 34 ++ .../DeleteAccountCommandValidator.cs | 14 + .../UpdateAccount/UpdateAccountCommand.cs | 18 ++ .../UpdateAccountCommandAuthorizer.cs | 31 ++ .../UpdateAccountCommandHandler.cs | 109 +++++++ .../UpdateAccountCommandValidator.cs | 68 ++++ .../Queries/GetAccount/GetAccountQuery.cs | 8 + .../GetAccount/GetAccountQueryAuthorizer.cs | 31 ++ .../GetAccount/GetAccountQueryHandler.cs | 51 +++ .../GetAccount/GetAccountQueryValidator.cs | 14 + .../GetAccountsPage/GetAccountsPageQuery.cs | 18 ++ .../GetAccountsPageQueryAuthorizer.cs | 31 ++ .../GetAccountsPageQueryHandler.cs | 81 +++++ .../GetAccountsPageQueryValidator.cs | 43 +++ .../ViewModels/AddAccountViewModel.cs | 2 + .../GetAccountsPageFilterViewModel.cs | 6 + .../ViewModels/UpdateAccountViewModel.cs | 14 + src/HttpApi/Controllers/IdentityController.cs | 290 +++++++++--------- 24 files changed, 813 insertions(+), 165 deletions(-) create mode 100644 src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs create mode 100644 src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs create mode 100644 src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs create mode 100644 src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs create mode 100644 src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs create mode 100644 src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs create mode 100644 src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs create mode 100644 src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs create mode 100644 src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs create mode 100644 src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs create mode 100644 src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs index cf5ea4f..eff0d6d 100644 --- a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs @@ -1,10 +1,13 @@ using cuqmbr.TravelGuide.Domain.Enums; using MediatR; -namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.AddAccount; public record AddAccountCommand : IRequest { + public string Username { get; set; } + public string Email { get; set; } public string Password { get; set; } diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs index 07ff887..2e62d6b 100644 --- a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs @@ -3,7 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; -namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; public class AddAccountCommandAuthorizer : AbstractRequestAuthorizer diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs index 0d54557..149baca 100644 --- a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs @@ -7,34 +7,33 @@ using cuqmbr.TravelGuide.Application.Common.Services; using System.Security.Cryptography; using System.Text; -namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; public class AddAccountCommandHandler : IRequestHandler { private readonly UnitOfWork _unitOfWork; private readonly IMapper _mapper; - private readonly PasswordHasherService _passwordHasherService; + private readonly PasswordHasherService _passwordHasher; - public AddAccountCommandHandler( - UnitOfWork unitOfWork, - IMapper mapper, - PasswordHasherService passwordHasherService) + public AddAccountCommandHandler(UnitOfWork unitOfWork, + IMapper mapper, PasswordHasherService passwordHasher) { _unitOfWork = unitOfWork; _mapper = mapper; - _passwordHasherService = passwordHasherService; + _passwordHasher = passwordHasher; } public async Task Handle( AddAccountCommand request, CancellationToken cancellationToken) { - var user = await _unitOfWork.AccountRepository.GetOneAsync( + var account = await _unitOfWork.AccountRepository.GetOneAsync( e => e.Email == request.Email, cancellationToken); - if (user != null) + if (account != null) { throw new DuplicateEntityException(); } @@ -47,15 +46,16 @@ public class AddAccountCommandHandler : .Items; var salt = RandomNumberGenerator.GetBytes(128 / 8); - var hash = await _passwordHasherService.HashAsync( + var hash = await _passwordHasher.HashAsync( Encoding.UTF8.GetBytes(request.Password), salt, cancellationToken); var saltBase64 = Convert.ToBase64String(salt); var hashBase64 = Convert.ToBase64String(hash); - user = new Account() + account = new Account() { + Username = request.Username, Email = request.Email, PasswordHash = hashBase64, PasswordSalt = saltBase64, @@ -66,12 +66,12 @@ public class AddAccountCommandHandler : .ToArray() }; - user = await _unitOfWork.AccountRepository.AddOneAsync( - user, cancellationToken); + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); await _unitOfWork.SaveAsync(cancellationToken); _unitOfWork.Dispose(); - return _mapper.Map(user); + return _mapper.Map(account); } } diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs index 62586db..85e91c5 100644 --- a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs @@ -1,16 +1,37 @@ using cuqmbr.TravelGuide.Application.Common.FluentValidation; using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; using Microsoft.Extensions.Localization; -namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; -public class AddAccountCommandValidator : AbstractValidator +public class AddAccountCommandValidator : + AbstractValidator { public AddAccountCommandValidator( IStringLocalizer localizer, SessionCultureService cultureService) { + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + RuleFor(v => v.Email) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]) @@ -32,5 +53,18 @@ public class AddAccountCommandValidator : AbstractValidator cultureService.Culture, localizer["FluentValidation.MaximumLength"], 64)); + + RuleFor(v => v.Roles ?? new IdentityRole[0]) + .IsUnique(r => r) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.Roles) + .Must(r => IdentityRole.Enumerations.ContainsValue(r)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + IdentityRole.Enumerations.Values.Select(e => e.Name)))); } } diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs new file mode 100644 index 0000000..0d673af --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.DeleteAccount; + +public record DeleteAccountCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs new file mode 100644 index 0000000..4019940 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAccountCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs new file mode 100644 index 0000000..26dec51 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAccountCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAccountCommand request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AccountRepository.DeleteOneAsync( + account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs new file mode 100644 index 0000000..1baf7f2 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandValidator : AbstractValidator +{ + public DeleteAccountCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs new file mode 100644 index 0000000..b5ed1e2 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs @@ -0,0 +1,18 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public record UpdateAccountCommand : IRequest +{ + public Guid Guid { get; set; } + + public string? Username { get; set; } + + public string? Email { get; set; } + + public string? Password { get; set; } + + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs new file mode 100644 index 0000000..ed54a5a --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAccountCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs new file mode 100644 index 0000000..8a64931 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs @@ -0,0 +1,109 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasher; + + public UpdateAccountCommandHandler(UnitOfWork unitOfWork, + IMapper mapper, PasswordHasherService passwordHasher) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _passwordHasher = passwordHasher; + } + + public async Task Handle( + UpdateAccountCommand request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository + .GetOneAsync(e => e.Guid == request.Guid, + e => e.AccountRoles, cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + + account.Username = request.Username ?? account.Username; + account.Email = request.Email ?? account.Email; + + if (request.Password != null) + { + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account.PasswordHash = hashBase64; + account.PasswordSalt = saltBase64; + } + + + if (request.Roles != null) + { + var requestRoleIds = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => request.Roles.Contains(r.Value), + 1, request.Roles.Count, cancellationToken)) + .Items + .Select(r => r.Id); + + var accountRoles = account.AccountRoles; + var accountRoleIds = accountRoles.Select(ar => ar.RoleId); + + var commonRoleIds = requestRoleIds.Intersect(accountRoleIds); + + var newRoleIds = requestRoleIds.Except(accountRoleIds); + + var combinedRoleIds = commonRoleIds.Union(newRoleIds); + + account.AccountRoles = combinedRoleIds.Select(rId => + new AccountRole() + { + Id = accountRoles.FirstOrDefault(ar => + ar.RoleId == rId)?.Id ?? default, + RoleId = rId + }) + .ToList(); + } + else + { + var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId); + var accountRoles = (await _unitOfWork.AccountRoleRepository + .GetPageAsync( + ar => accountRoleIds.Contains(ar.RoleId), + ar => ar.Role, + 1, accountRoleIds.Count(), cancellationToken)) + .Items; + + account.AccountRoles = accountRoles.ToList(); + } + + + account = await _unitOfWork.AccountRepository.UpdateOneAsync( + account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(account); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs new file mode 100644 index 0000000..ab00393 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs @@ -0,0 +1,68 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandValidator : + AbstractValidator +{ + public UpdateAccountCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Username) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Roles ?? new IdentityRole[0]) + .IsUnique(r => r) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.Roles) + .Must(r => IdentityRole.Enumerations.ContainsValue(r)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + IdentityRole.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs new file mode 100644 index 0000000..0031fcc --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public record GetAccountQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs new file mode 100644 index 0000000..56954d3 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAccountQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAccountQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs new file mode 100644 index 0000000..afbf47a --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAccountQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAccountQuery request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.AccountRoles, + cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + + var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId); + var accountRoles = (await _unitOfWork.AccountRoleRepository + .GetPageAsync( + ar => accountRoleIds.Contains(ar.RoleId), + ar => ar.Role, + 1, accountRoleIds.Count(), cancellationToken)) + .Items; + + account.AccountRoles = accountRoles.ToList(); + + + _unitOfWork.Dispose(); + + return _mapper.Map(account); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs new file mode 100644 index 0000000..d0b26ab --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryValidator : AbstractValidator +{ + public GetAccountQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs new file mode 100644 index 0000000..252f9c5 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs @@ -0,0 +1,18 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public record GetAccountsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs new file mode 100644 index 0000000..79d158a --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAccountsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAccountsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs new file mode 100644 index 0000000..dc5f975 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs @@ -0,0 +1,81 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAccountsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAccountsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AccountRepository.GetPageAsync( + a => + (a.Username.ToLower().Contains(request.Search.ToLower()) || + a.Email.ToLower().Contains(request.Search.ToLower())) && + (request.Roles != null + ? request.Roles.All(r => a.AccountRoles.Any(ar => ar.Role.Value == r)) + : true), + a => a.AccountRoles, + request.PageNumber, request.PageSize, cancellationToken); + + + var accounts = paginatedList.Items; + + var accountsRoleIds = accounts + .SelectMany(a => a.AccountRoles) + .Select(ar => ar.RoleId) + .Distinct(); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => accountsRoleIds.Contains(r.Id), + 1, accountsRoleIds.Count(), cancellationToken)) + .Items; + + foreach (var account in accounts) + { + account.AccountRoles = account.AccountRoles.Select(ar => + new AccountRole() + { + RoleId = ar.RoleId, + Role = roles.Single(r => r.Id == ar.RoleId), + AccountId = account.Id, + Account = account + }) + .ToArray(); + } + + + var mappedItems = _mapper + .ProjectTo(accounts.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + + throw new NotImplementedException(); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs new file mode 100644 index 0000000..d42a9ac --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryValidator : AbstractValidator +{ + public GetAccountsPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs index c8c8bc3..5ff18bc 100644 --- a/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs +++ b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs @@ -2,6 +2,8 @@ namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; public sealed class AddAccountViewModel { + public string Username { get; set; } + public string Email { get; set; } public string Password { get; set; } diff --git a/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs b/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs new file mode 100644 index 0000000..d95a4a6 --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class GetAccountsPageFilterViewModel +{ + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs b/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs new file mode 100644 index 0000000..4ae0732 --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class UpdateAccountViewModel +{ + public Guid Uuid { get; set; } + + public string? Username { get; set; } + + public string? Email { get; set; } + + public string? Password { get; set; } + + public ICollection? Roles { get; set; } +} diff --git a/src/HttpApi/Controllers/IdentityController.cs b/src/HttpApi/Controllers/IdentityController.cs index bacb072..967da73 100644 --- a/src/HttpApi/Controllers/IdentityController.cs +++ b/src/HttpApi/Controllers/IdentityController.cs @@ -7,12 +7,10 @@ using cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; using cuqmbr.TravelGuide.Application.Identity.Accounts; using cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; -// using cuqmbr.TravelGuide.Application.Identity.Commands.AddIdentity; -// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentityPage; -// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentity; -// using cuqmbr.TravelGuide.Application.Identity.Commands.UpdateIdentity; -// using cuqmbr.TravelGuide.Application.Identity.Commands.DeleteIdentity; -// using cuqmbr.TravelGuide.Application.Identity.ViewModels; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -78,7 +76,7 @@ public class IdentityController : ControllerBase [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] - public async Task> Add( + public async Task> AddAccount( [FromBody] AddAccountViewModel viewModel, CancellationToken cancellationToken) { @@ -87,6 +85,7 @@ public class IdentityController : ControllerBase await Mediator.Send( new AddAccountCommand() { + Username = viewModel.Username, Email = viewModel.Email, Password = viewModel.Password, Roles = viewModel.Roles @@ -96,147 +95,144 @@ public class IdentityController : ControllerBase cancellationToken)); } + [HttpGet("accounts")] + [SwaggerOperation("Get a list of all accounts")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetAccountsPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAccountsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAccountsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + Roles = filterQuery.Roles == null ? null : + filterQuery.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken); + } + [HttpGet("accounts/{uuid:guid}")] + [SwaggerOperation("Get an account by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AccountDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task GetAccount( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAccountQuery() { Guid = uuid }, + cancellationToken); + } + [HttpPut("accounts/{uuid:guid}")] + [SwaggerOperation("Update an account")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AccountDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task UpdateAccount( + [FromRoute] Guid uuid, + [FromBody] UpdateAccountViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAccountCommand() + { + Guid = uuid, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password, + Roles = viewModel.Roles == null ? null : + viewModel.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken); + } - // [HttpPost] - // [SwaggerOperation("Add an identity")] - // [SwaggerResponse( - // StatusCodes.Status201Created, "Object successfuly created", - // typeof(IdentityDto))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Object already exists", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Parent object not found", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task> Add( - // [FromBody] AddIdentityViewModel viewModel, - // CancellationToken cancellationToken) - // { - // return StatusCode( - // StatusCodes.Status201Created, - // await Mediator.Send( - // new AddIdentityCommand() - // { - // Name = viewModel.Name, - // Longitude = viewModel.Longitude, - // Latitude = viewModel.Latitude, - // VehicleType = VehicleType.FromName(viewModel.VehicleType), - // CityGuid = viewModel.CityUuid - // }, - // cancellationToken)); - // } - // - // [HttpGet("{uuid:guid}")] - // [SwaggerOperation("Get an identity by uuid")] - // [SwaggerResponse( - // StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Get( - // [FromRoute] Guid uuid, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send(new GetIdentityQuery() { Guid = uuid }, - // cancellationToken); - // } - // - // [HttpPut("{uuid:guid}")] - // [SwaggerOperation("Update an identity")] - // [SwaggerResponse( - // StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Object already exists", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Parent object not found", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Update( - // [FromRoute] Guid uuid, - // [FromBody] UpdateIdentityViewModel viewModel, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send( - // new UpdateIdentityCommand() - // { - // Guid = uuid, - // Name = viewModel.Name, - // Longitude = viewModel.Longitude, - // Latitude = viewModel.Latitude, - // VehicleType = VehicleType.FromName(viewModel.VehicleType), - // CityGuid = viewModel.CityUuid - // }, - // cancellationToken); - // } - // - // [HttpDelete("{uuid:guid}")] - // [SwaggerOperation("Delete an identity")] - // [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Delete( - // [FromRoute] Guid uuid, - // CancellationToken cancellationToken) - // { - // await Mediator.Send( - // new DeleteIdentityCommand() { Guid = uuid }, - // cancellationToken); - // return StatusCode(StatusCodes.Status204NoContent); - // } + [HttpDelete("accounts/{uuid:guid}")] + [SwaggerOperation("Delete an account")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task DeleteAccount( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAccountCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } } From bb309d7c207df3e51382a8b8c0c11516b8c900dc Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 28 May 2025 17:55:32 +0300 Subject: [PATCH 27/35] add account creation when adding an employee --- .../FluentValidation/CustomValidators.cs | 4 +- .../AddEmployee/AddEmployeeCommand.cs | 7 + .../AddEmployee/AddEmployeeCommandHandler.cs | 55 +- .../AddEmployeeCommandValidator.cs | 42 + .../DeleteEmployeeCommandHandler.cs | 5 +- .../UpdateEmployeeCommandHandler.cs | 40 +- .../Employees/EmployeeAccountDto.cs | 11 + src/Application/Employees/EmployeeDto.cs | 2 + .../GetEmployee/GetEmployeeQueryHandler.cs | 7 +- .../GetEmployeesPageQueryHandler.cs | 12 +- .../ViewModels/AddEmployeeViewModel.cs | 7 + src/Domain/Entities/Account.cs | 3 + src/Domain/Entities/Employee.cs | 5 + .../Controllers/EmployeesController.cs | 5 +- .../Configurations/EmployeeConfiguration.cs | 24 + ...ation_from_Employee_to_Account.Designer.cs | 1313 +++++++++++++++++ ...Add_navigation_from_Employee_to_Account.cs | 58 + .../PostgreSqlDbContextModelSnapshot.cs | 19 + 18 files changed, 1590 insertions(+), 29 deletions(-) create mode 100644 src/Application/Employees/EmployeeAccountDto.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs index 0dfbce8..aa7bb49 100644 --- a/src/Application/Common/FluentValidation/CustomValidators.cs +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -9,7 +9,7 @@ public static class CustomValidators { return ruleBuilder - .Matches(@"^[a-z0-9-_.]*$"); + .Matches(@"^[a-z0-9-_\.]*$"); } // According to RFC 5321. @@ -18,7 +18,7 @@ public static class CustomValidators { return ruleBuilder - .Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$"); + .Matches(@"^[a-z0-9-_\.]{1,64}@[a-z0-9-_\.]{1,251}\.[a-z0-9-_]{2,4}$"); } // According to ITU-T E.164, no spaces. diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs index 4fe21d7..073c181 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs @@ -20,4 +20,11 @@ public record AddEmployeeCommand : IRequest public Guid CompanyGuid { get; set; } public ICollection Documents { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } } diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs index adb5fa0..610f601 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -4,6 +4,10 @@ using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Enums; +using System.Security.Cryptography; +using cuqmbr.TravelGuide.Application.Common.Services; +using System.Text; namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; @@ -13,15 +17,15 @@ public class AddEmployeeCommandHandler : private readonly UnitOfWork _unitOfWork; private readonly IMapper _mapper; private readonly IStringLocalizer _localizer; + private readonly PasswordHasherService _passwordHasher; - public AddEmployeeCommandHandler( - UnitOfWork unitOfWork, - IMapper mapper, - IStringLocalizer localizer) + public AddEmployeeCommandHandler(UnitOfWork unitOfWork, IMapper mapper, + IStringLocalizer localizer, PasswordHasherService passwordHasher) { _unitOfWork = unitOfWork; _mapper = mapper; _localizer = localizer; + _passwordHasher = passwordHasher; } public async Task Handle( @@ -52,6 +56,45 @@ public class AddEmployeeCommandHandler : throw new DuplicateEntityException(); } + + // Create new account for employee + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (account != null) + { + throw new DuplicateEntityException(); + } + + + var role = (await _unitOfWork.RoleRepository.GetPageAsync( + 1, IdentityRole.Enumerations.Count(), cancellationToken)) + .Items + .First(r => r.Value.Equals(IdentityRole.CompanyEmployee)); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account = new Account() + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = new AccountRole[] { new() { RoleId = role.Id } } + }; + + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); + + entity = new Employee() { FirstName = request.FirstName, @@ -66,12 +109,14 @@ public class AddEmployeeCommandHandler : Information = d.Information }) .ToArray(), - Company = parentEntity + Company = parentEntity, + Account = account }; entity = await _unitOfWork.EmployeeRepository.AddOneAsync( entity, cancellationToken); + await _unitOfWork.SaveAsync(cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs index 3a9348e..5e7a87a 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs @@ -1,3 +1,4 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Domain.Enums; using FluentValidation; @@ -79,5 +80,46 @@ public class AddEmployeeCommandValidator : AbstractValidator localizer["FluentValidation.MaximumLength"], 256)); }); + + + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); } } diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs index 539ebcf..a4f6233 100644 --- a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs @@ -18,7 +18,7 @@ public class DeleteEmployeeCommandHandler : IRequestHandler e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Account, cancellationToken); if (entity == null) { @@ -31,6 +31,9 @@ public class DeleteEmployeeCommandHandler : IRequestHandler e.FirstName == request.FirstName && e.LastName == request.LastName && @@ -49,30 +49,34 @@ public class UpdateEmployeeCommandHandler : e.Guid != request.Guid, cancellationToken); - if (entity != null) + if (employee != null) { throw new DuplicateEntityException(); } - entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + employee = await _unitOfWork.EmployeeRepository.GetOneAsync( e => e.Guid == request.Guid, e => e.Documents, cancellationToken); - if (entity == null) + if (employee == null) { throw new NotFoundException(); } + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == employee.AccountId, cancellationToken); - entity.Guid = request.Guid; - entity.FirstName = request.FirstName; - entity.LastName = request.LastName; - entity.Patronymic = request.Patronymic; - entity.Sex = request.Sex; - entity.BirthDate = request.BirthDate; - entity.CompanyId = parentEntity.Id; - entity.Company = parentEntity; + employee.Guid = request.Guid; + employee.FirstName = request.FirstName; + employee.LastName = request.LastName; + employee.Patronymic = request.Patronymic; + employee.Sex = request.Sex; + employee.BirthDate = request.BirthDate; + employee.CompanyId = parentEntity.Id; + + employee.Company = parentEntity; + employee.Account = account; var requestEmployeeDocuments = request.Documents.Select( @@ -82,27 +86,27 @@ public class UpdateEmployeeCommandHandler : Information = d.Information }); - var commonEmployeeDocuments = entity.Documents.IntersectBy( + var commonEmployeeDocuments = employee.Documents.IntersectBy( requestEmployeeDocuments.Select( ed => (ed.DocumentType, ed.Information)), ed => (ed.DocumentType, ed.Information)); var newEmployeeDocuments = requestEmployeeDocuments.ExceptBy( - entity.Documents.Select(ed => (ed.DocumentType, ed.Information)), + employee.Documents.Select(ed => (ed.DocumentType, ed.Information)), ed => (ed.DocumentType, ed.Information)); var combinedEmployeeDocuments = commonEmployeeDocuments.UnionBy( newEmployeeDocuments, ed => (ed.DocumentType, ed.Information)); - entity.Documents = combinedEmployeeDocuments.ToList(); + employee.Documents = combinedEmployeeDocuments.ToList(); - entity = await _unitOfWork.EmployeeRepository.UpdateOneAsync( - entity, cancellationToken); + employee = await _unitOfWork.EmployeeRepository.UpdateOneAsync( + employee, cancellationToken); await _unitOfWork.SaveAsync(cancellationToken); _unitOfWork.Dispose(); - return _mapper.Map(entity); + return _mapper.Map(employee); } } diff --git a/src/Application/Employees/EmployeeAccountDto.cs b/src/Application/Employees/EmployeeAccountDto.cs new file mode 100644 index 0000000..3f603bd --- /dev/null +++ b/src/Application/Employees/EmployeeAccountDto.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeAccountDto : IMapFrom +{ + public string Username { get; set; } + + public string Email { get; set; } +} diff --git a/src/Application/Employees/EmployeeDto.cs b/src/Application/Employees/EmployeeDto.cs index fec500f..ff20575 100644 --- a/src/Application/Employees/EmployeeDto.cs +++ b/src/Application/Employees/EmployeeDto.cs @@ -22,6 +22,8 @@ public sealed class EmployeeDto : IMapFrom public ICollection Documents { get; set; } + public EmployeeAccountDto Account { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs index f8f7517..75eba22 100644 --- a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs @@ -33,13 +33,18 @@ public class GetEmployeeQueryHandler : } - // Hydrate employees with companies + // Hydrate employee var company = await _unitOfWork.CompanyRepository.GetOneAsync( e => e.Id == entity.CompanyId, cancellationToken); entity.Company = company; + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Id == entity.AccountId, cancellationToken); + + entity.Account = account; + _unitOfWork.Dispose(); diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs index ba18ac0..a74cd58 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -49,7 +49,7 @@ public class GetEmployeesPageQueryHandler : cancellationToken); - // Hydrate employees with companies + // Hydrate employees var companies = await _unitOfWork.CompanyRepository.GetPageAsync( e => paginatedList.Items.Select(e => e.CompanyId).Contains(e.Id), @@ -61,6 +61,16 @@ public class GetEmployeesPageQueryHandler : companies.Items.First(c => c.Id == employee.CompanyId); } + var accounts = await _unitOfWork.AccountRepository.GetPageAsync( + e => paginatedList.Items.Select(e => e.AccountId).Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var employee in paginatedList.Items) + { + employee.Account = + accounts.Items.First(a => a.Id == employee.AccountId); + } + var mappedItems = _mapper .ProjectTo(paginatedList.Items.AsQueryable()); diff --git a/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs index 97ba828..c058d22 100644 --- a/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs +++ b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs @@ -16,4 +16,11 @@ public sealed class AddEmployeeViewModel public Guid CompanyUuid { get; set; } public ICollection Documents { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } } diff --git a/src/Domain/Entities/Account.cs b/src/Domain/Entities/Account.cs index b477df4..a2c574a 100644 --- a/src/Domain/Entities/Account.cs +++ b/src/Domain/Entities/Account.cs @@ -13,4 +13,7 @@ public sealed class Account : EntityBase public ICollection AccountRoles { get; set; } public ICollection RefreshTokens { get; set; } + + + public Employee? Employee { get; set; } } diff --git a/src/Domain/Entities/Employee.cs b/src/Domain/Entities/Employee.cs index 3035cad..d89d2ce 100644 --- a/src/Domain/Entities/Employee.cs +++ b/src/Domain/Entities/Employee.cs @@ -22,4 +22,9 @@ public sealed class Employee : EntityBase public ICollection Documents { get; set; } public ICollection VehicleEnrollmentEmployees { get; set; } + + + public long AccountId { get; set; } + + public Account Account { get; set; } } diff --git a/src/HttpApi/Controllers/EmployeesController.cs b/src/HttpApi/Controllers/EmployeesController.cs index 4df1d5b..2af1e28 100644 --- a/src/HttpApi/Controllers/EmployeesController.cs +++ b/src/HttpApi/Controllers/EmployeesController.cs @@ -59,7 +59,10 @@ public class EmployeesController : ControllerBase Information = e.Information }).ToArray(), - CompanyGuid = viewModel.CompanyUuid + CompanyGuid = viewModel.CompanyUuid, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password }, cancellationToken)); } diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs index fd4ccb5..3aae0e9 100644 --- a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs @@ -77,5 +77,29 @@ public class EmployeeConfiguration : BaseConfiguration "ix_" + $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}"); + + + builder + .Property(e => e.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(e => e.Account) + .WithOne(a => a.Employee) + .HasForeignKey(e => e.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(e => e.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs new file mode 100644 index 0000000..bd4a395 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs @@ -0,0 +1,1313 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250528141733_Add_navigation_from_Employee_to_Account")] + partial class Add_navigation_from_Employee_to_Account + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs new file mode 100644 index 0000000..56f22dd --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Employee_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "employees", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateIndex( + name: "ix_employees_account_id", + schema: "application", + table: "employees", + column: "account_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_employees_account_id", + schema: "application", + table: "employees", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_employees_account_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropIndex( + name: "ix_employees_account_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "employees"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index ba23151..f9e33c5 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -306,6 +306,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + b.Property("BirthDate") .HasColumnType("date") .HasColumnName("birth_date"); @@ -344,6 +348,10 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_employees_uuid"); + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + b.HasIndex("CompanyId") .HasDatabaseName("ix_employees_company_id"); @@ -1030,6 +1038,13 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") .WithMany("Employees") .HasForeignKey("CompanyId") @@ -1037,6 +1052,8 @@ namespace Persistence.PostgreSql.Migrations .IsRequired() .HasConstraintName("fk_employees_company_id"); + b.Navigation("Account"); + b.Navigation("Company"); }); @@ -1213,6 +1230,8 @@ namespace Persistence.PostgreSql.Migrations { b.Navigation("AccountRoles"); + b.Navigation("Employee"); + b.Navigation("RefreshTokens"); }); From 9ccd0bb68d1d5c3d325558e115a49819d8c8e592 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 11:56:34 +0300 Subject: [PATCH 28/35] add account creation when adding a company --- .../Commands/AddCompany/AddCompanyCommand.cs | 7 + .../AddCompany/AddCompanyCommandHandler.cs | 56 +- .../AddCompany/AddCompanyCommandValidator.cs | 41 + .../DeleteCompanyCommandHandler.cs | 5 +- .../UpdateCompanyCommandHandler.cs | 4 + .../Companies/CompanyAccountDto.cs | 21 + src/Application/Companies/CompanyDto.cs | 2 + .../GetCompaniesPageQueryHandler.cs | 13 + .../GetCompany/GetCompanyQueryHandler.cs | 9 +- .../ViewModels/AddCompanyViewModel.cs | 7 + .../AddEmployee/AddEmployeeCommandHandler.cs | 1 - .../Employees/EmployeeAccountDto.cs | 10 + .../GetEmployeesPageQueryHandler.cs | 3 +- src/Domain/Entities/Account.cs | 2 + src/Domain/Entities/Company.cs | 5 + .../Controllers/CompaniesController.cs | 3 + .../Configurations/CompanyConfiguration.cs | 24 + ...gation_from_Company_to_Account.Designer.cs | 1335 +++++++++++++++++ ..._Add_navigation_from_Company_to_Account.cs | 58 + .../PostgreSqlDbContextModelSnapshot.cs | 22 + 20 files changed, 1617 insertions(+), 11 deletions(-) create mode 100644 src/Application/Companies/CompanyAccountDto.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs index 5965a59..82aed33 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs @@ -11,4 +11,11 @@ public record AddCompanyCommand : IRequest public string ContactEmail { get; set; } public string ContactPhoneNumber { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } } diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs index 2605a86..8665252 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs @@ -1,8 +1,12 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Persistence; -using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using System.Security.Cryptography; +using System.Text; namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; @@ -11,13 +15,14 @@ public class AddCompanyCommandHandler : { private readonly UnitOfWork _unitOfWork; private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasher; - public AddCompanyCommandHandler( - UnitOfWork unitOfWork, - IMapper mapper) + public AddCompanyCommandHandler(UnitOfWork unitOfWork, IMapper mapper, + PasswordHasherService passwordHasher) { _unitOfWork = unitOfWork; _mapper = mapper; + _passwordHasher = passwordHasher; } public async Task Handle( @@ -33,12 +38,51 @@ public class AddCompanyCommandHandler : "Company with given name already exists."); } + + // Create new account for employee + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (account != null) + { + throw new DuplicateEntityException(); + } + + var role = (await _unitOfWork.RoleRepository.GetPageAsync( + 1, IdentityRole.Enumerations.Count(), cancellationToken)) + .Items + .First(r => r.Value.Equals(IdentityRole.CompanyOwner)); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account = new Account() + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = new AccountRole[] { new() { RoleId = role.Id } } + }; + + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); + + entity = new Company() { Name = request.Name, LegalAddress = request.LegalAddress, ContactEmail = request.ContactEmail, - ContactPhoneNumber = request.ContactPhoneNumber + ContactPhoneNumber = request.ContactPhoneNumber, + Account = account }; entity = await _unitOfWork.CompanyRepository.AddOneAsync( diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs index 5e522a4..8718e95 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs @@ -54,5 +54,46 @@ public class AddCompanyCommandValidator : AbstractValidator cultureService.Culture, localizer["FluentValidation.MaximumLength"], 64)); + + + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); } } diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs index da0a471..ec65227 100644 --- a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs @@ -18,7 +18,7 @@ public class DeleteCompanyCommandHandler : IRequestHandler CancellationToken cancellationToken) { var entity = await _unitOfWork.CompanyRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Account, cancellationToken); if (entity == null) { @@ -28,6 +28,9 @@ public class DeleteCompanyCommandHandler : IRequestHandler await _unitOfWork.CompanyRepository.DeleteOneAsync( entity, cancellationToken); + await _unitOfWork.AccountRepository.DeleteOneAsync( + entity.Account, cancellationToken); + await _unitOfWork.SaveAsync(cancellationToken); _unitOfWork.Dispose(); } diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs index e57cda5..bcab8c5 100644 --- a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs @@ -31,10 +31,14 @@ public class UpdateCompanyCommandHandler : throw new NotFoundException(); } + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == entity.AccountId, cancellationToken); + entity.Name = request.Name; entity.LegalAddress = request.LegalAddress; entity.ContactEmail = request.ContactEmail; entity.ContactPhoneNumber = request.ContactPhoneNumber; + entity.Account = account; entity = await _unitOfWork.CompanyRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Companies/CompanyAccountDto.cs b/src/Application/Companies/CompanyAccountDto.cs new file mode 100644 index 0000000..a428a7d --- /dev/null +++ b/src/Application/Companies/CompanyAccountDto.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Companies; + +public sealed class CompanyAccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Companies/CompanyDto.cs b/src/Application/Companies/CompanyDto.cs index 3bdc207..47fd659 100644 --- a/src/Application/Companies/CompanyDto.cs +++ b/src/Application/Companies/CompanyDto.cs @@ -15,6 +15,8 @@ public sealed class CompanyDto : IMapFrom public string ContactPhoneNumber { get; set; } + public CompanyAccountDto Account { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs index 3749deb..62ea670 100644 --- a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs @@ -33,6 +33,19 @@ public class GetCompaniesPageQueryHandler : request.PageNumber, request.PageSize, cancellationToken); + // Hydrate companies + + var accountIds = paginatedList.Items.Select(e => e.AccountId); + var accounts = await _unitOfWork.AccountRepository.GetPageAsync( + e => accountIds.Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var company in paginatedList.Items) + { + company.Account = + accounts.Items.First(a => a.Id == company.AccountId); + } + var mappedItems = _mapper .ProjectTo(paginatedList.Items.AsQueryable()); diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs index a625857..9388c14 100644 --- a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs @@ -26,13 +26,18 @@ public class GetCompanyQueryHandler : var entity = await _unitOfWork.CompanyRepository.GetOneAsync( e => e.Guid == request.Guid, cancellationToken); - _unitOfWork.Dispose(); - if (entity == null) { throw new NotFoundException(); } + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Id == entity.AccountId, cancellationToken); + + entity.Account = account; + + _unitOfWork.Dispose(); + return _mapper.Map(entity); } } diff --git a/src/Application/Companies/ViewModels/AddCompanyViewModel.cs b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs index 2927de8..253696e 100644 --- a/src/Application/Companies/ViewModels/AddCompanyViewModel.cs +++ b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs @@ -9,4 +9,11 @@ public sealed class AddCompanyViewModel public string ContactEmail { get; set; } public string ContactPhoneNumber { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } } diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs index 610f601..63857c4 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -68,7 +68,6 @@ public class AddEmployeeCommandHandler : throw new DuplicateEntityException(); } - var role = (await _unitOfWork.RoleRepository.GetPageAsync( 1, IdentityRole.Enumerations.Count(), cancellationToken)) .Items diff --git a/src/Application/Employees/EmployeeAccountDto.cs b/src/Application/Employees/EmployeeAccountDto.cs index 3f603bd..ced3c91 100644 --- a/src/Application/Employees/EmployeeAccountDto.cs +++ b/src/Application/Employees/EmployeeAccountDto.cs @@ -5,7 +5,17 @@ namespace cuqmbr.TravelGuide.Application.Employees; public sealed class EmployeeAccountDto : IMapFrom { + public Guid Uuid { get; set; } + public string Username { get; set; } public string Email { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } } diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs index a74cd58..20a2e66 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -61,8 +61,9 @@ public class GetEmployeesPageQueryHandler : companies.Items.First(c => c.Id == employee.CompanyId); } + var accountIds = paginatedList.Items.Select(e => e.AccountId); var accounts = await _unitOfWork.AccountRepository.GetPageAsync( - e => paginatedList.Items.Select(e => e.AccountId).Contains(e.Id), + e => accountIds.Contains(e.Id), 1, paginatedList.Items.Count, cancellationToken); foreach (var employee in paginatedList.Items) diff --git a/src/Domain/Entities/Account.cs b/src/Domain/Entities/Account.cs index a2c574a..b3b0464 100644 --- a/src/Domain/Entities/Account.cs +++ b/src/Domain/Entities/Account.cs @@ -16,4 +16,6 @@ public sealed class Account : EntityBase public Employee? Employee { get; set; } + + public Company? Company { get; set; } } diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs index cab7f8e..553a601 100644 --- a/src/Domain/Entities/Company.cs +++ b/src/Domain/Entities/Company.cs @@ -14,4 +14,9 @@ public sealed class Company : EntityBase public ICollection Employees { get; set; } public ICollection Vehicles { get; set; } + + + public long AccountId { get; set; } + + public Account Account { get; set; } } diff --git a/src/HttpApi/Controllers/CompaniesController.cs b/src/HttpApi/Controllers/CompaniesController.cs index a8ca669..617f3ff 100644 --- a/src/HttpApi/Controllers/CompaniesController.cs +++ b/src/HttpApi/Controllers/CompaniesController.cs @@ -52,6 +52,9 @@ public class CompaniesController : ControllerBase LegalAddress = viewModel.LegalAddress, ContactEmail = viewModel.ContactEmail, ContactPhoneNumber = viewModel.ContactPhoneNumber, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password }, cancellationToken)); } diff --git a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs index b01666c..bc1b29b 100644 --- a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs @@ -38,5 +38,29 @@ public class CompanyConfiguration : BaseConfiguration .HasColumnName("contact_phone_number") .HasColumnType("varchar(64)") .IsRequired(true); + + + builder + .Property(c => c.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(c => c.Account) + .WithOne(a => a.Company) + .HasForeignKey(c => c.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(c => c.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs new file mode 100644 index 0000000..8486e6b --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs @@ -0,0 +1,1335 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250528182232_Add_navigation_from_Company_to_Account")] + partial class Add_navigation_from_Company_to_Account + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs new file mode 100644 index 0000000..80c669c --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Company_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "companies", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateIndex( + name: "ix_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropIndex( + name: "ix_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "companies"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index f9e33c5..93002e6 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -235,6 +235,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + b.Property("ContactEmail") .IsRequired() .HasColumnType("varchar(256)") @@ -265,6 +269,10 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_companies_uuid"); + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + b.ToTable("companies", "application"); }); @@ -1036,6 +1044,18 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Region"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") @@ -1230,6 +1250,8 @@ namespace Persistence.PostgreSql.Migrations { b.Navigation("AccountRoles"); + b.Navigation("Company"); + b.Navigation("Employee"); b.Navigation("RefreshTokens"); From 41158b34c5fdf2f135a72ab185a4d52320909b40 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 12:24:13 +0300 Subject: [PATCH 29/35] flatten configuration file structure --- src/Application/ConfigurationOptions.cs | 2 +- .../Configuration/Configuration.cs | 15 ++--- .../Infrastructure/Configuration.cs | 5 +- src/HttpApi/appsettings.Development.json | 58 +++++++++---------- src/HttpApi/appsettings.json | 58 +++++++++---------- src/Infrastructure/ConfigurationOptions.cs | 6 +- src/Persistence/ConfigurationOptions.cs | 2 +- 7 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/Application/ConfigurationOptions.cs b/src/Application/ConfigurationOptions.cs index 7a6edea..8944ec8 100644 --- a/src/Application/ConfigurationOptions.cs +++ b/src/Application/ConfigurationOptions.cs @@ -2,7 +2,7 @@ namespace cuqmbr.TravelGuide.Application; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Application"; + public static string SectionName { get; } = ""; public LocalizationConfigurationOptions Localization { get; set; } = new(); diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index 923b7f8..d4b2ecc 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -25,17 +25,14 @@ public static class Configuration .AddCommandLine(args) .Build(); - services.AddOptions().Bind( - configuration.GetSection( - PersistenceConfigurationOptions.SectionName)); + services.AddOptions() + .Bind(configuration); - services.AddOptions().Bind( - configuration.GetSection( - ApplicationConfigurationOptions.SectionName)); + services.AddOptions() + .Bind(configuration); - services.AddOptions().Bind( - configuration.GetSection( - InfrastructureConfigurationOptions.SectionName)); + services.AddOptions() + .Bind(configuration); return services; } diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index 1c35fe6..3dd3f4b 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -21,7 +21,10 @@ public static class Configuration ExchangeApiCurrencyConverterService>() .AddScoped< cuqmbr.TravelGuide.Application.Common.Services.LiqPayPaymentService, - cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>(); + cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>() + .AddScoped< + EmailSenderService, + MailKitEmailSenderService>(); return services; } diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 81ff06d..05be9dc 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -1,35 +1,31 @@ { - "Application": { - "Logging": { - "Type": "SimpleConsole", - "LogLevel": "Information", - "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", - "UseUtcTimestamp": true - }, - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" - }, - "Localization": { - "DefaultCultureName": "en-US", - "CacheDuration": "00:30:00" - }, - "JsonWebToken": { - "Issuer": "https://api.travel-guide.cuqmbr.xyz", - "Audience": "https://travel-guide.cuqmbr.xyz", - "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", - "AccessTokenValidity": "24:00:00", - "RefreshTokenValidity": "72:00:00" - }, - "Infrastructure": { - "PaymentProcessing": { - "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", - "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", - "LiqPay": { - "PublicKey": "sandbox_xxxxxxxxxxxx", - "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 81ff06d..05be9dc 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -1,35 +1,31 @@ { - "Application": { - "Logging": { - "Type": "SimpleConsole", - "LogLevel": "Information", - "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", - "UseUtcTimestamp": true - }, - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" - }, - "Localization": { - "DefaultCultureName": "en-US", - "CacheDuration": "00:30:00" - }, - "JsonWebToken": { - "Issuer": "https://api.travel-guide.cuqmbr.xyz", - "Audience": "https://travel-guide.cuqmbr.xyz", - "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", - "AccessTokenValidity": "24:00:00", - "RefreshTokenValidity": "72:00:00" - }, - "Infrastructure": { - "PaymentProcessing": { - "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", - "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", - "LiqPay": { - "PublicKey": "sandbox_xxxxxxxxxxxx", - "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index 338297e..b0642ee 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -2,9 +2,9 @@ namespace cuqmbr.TravelGuide.Infrastructure; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Application:Infrastructure"; + public static string SectionName { get; } = ""; - public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } + public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } = new(); } public sealed class PaymentProcessingConfigurationOptions @@ -13,7 +13,7 @@ public sealed class PaymentProcessingConfigurationOptions public string ResultAddressBase { get; set; } - public LiqPayConfigurationOptions LiqPay { get; set; } + public LiqPayConfigurationOptions LiqPay { get; set; } = new(); } public sealed class LiqPayConfigurationOptions diff --git a/src/Persistence/ConfigurationOptions.cs b/src/Persistence/ConfigurationOptions.cs index 67f5dd5..a239384 100644 --- a/src/Persistence/ConfigurationOptions.cs +++ b/src/Persistence/ConfigurationOptions.cs @@ -2,7 +2,7 @@ namespace cuqmbr.TravelGuide.Persistence; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Application:Datastore"; + public static string SectionName { get; } = "Datastore"; public string Type { get; set; } = "inmemory"; From 68a9e06eebb83fabe8d714a6b00d3d902d1d24b3 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 13:13:41 +0300 Subject: [PATCH 30/35] add email sender service --- .../Common/Services/EmailSenderService.cs | 7 +++ src/Configuration/packages.lock.json | 29 +++++++++++ src/HttpApi/Controllers/TestsController.cs | 33 +++++------- src/HttpApi/appsettings.Development.json | 11 ++++ src/HttpApi/appsettings.json | 11 ++++ src/HttpApi/packages.lock.json | 29 +++++++++++ src/Infrastructure/ConfigurationOptions.cs | 24 +++++++++ src/Infrastructure/Infrastructure.csproj | 1 + .../Services/MailKitEmailSenderService.cs | 50 +++++++++++++++++++ src/Infrastructure/packages.lock.json | 34 +++++++++++++ .../packages.lock.json | 29 +++++++++++ 11 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 src/Application/Common/Services/EmailSenderService.cs create mode 100644 src/Infrastructure/Services/MailKitEmailSenderService.cs diff --git a/src/Application/Common/Services/EmailSenderService.cs b/src/Application/Common/Services/EmailSenderService.cs new file mode 100644 index 0000000..706d96d --- /dev/null +++ b/src/Application/Common/Services/EmailSenderService.cs @@ -0,0 +1,7 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface EmailSenderService +{ + Task SendAsync(string[] addresses, string subject, string body, + CancellationToken cancellationToken); +} diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index bba215c..aed0299 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -164,11 +164,25 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -701,6 +715,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -769,6 +792,11 @@ "resolved": "5.0.0", "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -845,6 +873,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" } diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index 3e2abf0..73b7134 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Application.Common.Persistence; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -9,15 +8,13 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TestsController : ControllerBase { private readonly IStringLocalizer _localizer; - private readonly UnitOfWork _unitOfWork; + private readonly EmailSenderService _emailSender; - public TestsController( - SessionCultureService cultureService, - IStringLocalizer localizer, - UnitOfWork unitOfWork) + public TestsController(SessionCultureService cultureService, + IStringLocalizer localizer, EmailSenderService emailSender) { _localizer = localizer; - _unitOfWork = unitOfWork; + _emailSender = emailSender; } [HttpGet("getLocalizedString/{inputString}")] @@ -31,19 +28,15 @@ public class TestsController : ControllerBase [HttpGet("trigger")] public async Task Trigger(CancellationToken cancellationToken) { - // await _unitOfWork.BusRepository.AddOneAsync( - // new Domain.Entities.Bus() - // { - // Number = "AB1234MK", - // Model = "This is a fancy bus model", - // Capacity = 40 - // }, - // cancellationToken); - // - // await _unitOfWork.SaveAsync(cancellationToken); - // _unitOfWork.Dispose(); + var body = +@"Hello, friend! - var vehicles = await _unitOfWork.VehicleRepository - .GetPageAsync(1, 10, cancellationToken); +This is my email message for you. + +-- +Travel Guide Service +"; + + await _emailSender.SendAsync(new string[] { "cuqmbr@ya.ru" }, "Test subject", body, cancellationToken); } } diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 05be9dc..1fd9334 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -27,5 +27,16 @@ "PublicKey": "sandbox_xxxxxxxxxxxx", "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } + }, + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" + } } } diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 05be9dc..1fd9334 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -27,5 +27,16 @@ "PublicKey": "sandbox_xxxxxxxxxxxx", "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } + }, + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" + } } } diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index 02503f2..59d1bfe 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -106,6 +106,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -125,6 +130,15 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -848,6 +862,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Mono.TextTemplating": { "type": "Transitive", "resolved": "3.0.0", @@ -982,6 +1005,11 @@ "System.Composition.Runtime": "7.0.0" } }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -1109,6 +1137,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" } diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index b0642ee..87ac871 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -5,6 +5,8 @@ public sealed class ConfigurationOptions public static string SectionName { get; } = ""; public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } = new(); + + public EmailConfigurationOptions Email { get; set; } = new(); } public sealed class PaymentProcessingConfigurationOptions @@ -22,3 +24,25 @@ public sealed class LiqPayConfigurationOptions public string PrivateKey { get; set; } } + +public sealed class EmailConfigurationOptions +{ + public SmtpConfigurationOptions Smtp { get; set; } = new(); +} + +public sealed class SmtpConfigurationOptions +{ + public string Host { get; set; } + + public ushort Port { get; set; } + + public bool UseTls { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string SenderAddress { get; set; } + + public string SenderName { get; set; } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 2ae6a29..cf7f00a 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Infrastructure/Services/MailKitEmailSenderService.cs b/src/Infrastructure/Services/MailKitEmailSenderService.cs new file mode 100644 index 0000000..36eb50d --- /dev/null +++ b/src/Infrastructure/Services/MailKitEmailSenderService.cs @@ -0,0 +1,50 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class MailKitEmailSenderService : EmailSenderService +{ + + private readonly SmtpConfigurationOptions _configuration; + + public MailKitEmailSenderService( + IOptions configuration) + { + _configuration = configuration.Value.Email.Smtp; + } + + public async Task SendAsync(string[] addresses, string subject, + string body, CancellationToken cancellationToken) + { + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress( + _configuration.SenderName, _configuration.SenderAddress)); + foreach (var address in addresses) + { + message.To.Add(new MailboxAddress("", address)); + } + message.Subject = subject; + + message.Body = new TextPart("plain") + { + Text = body + }; + + + using var client = new SmtpClient(); + + await client.ConnectAsync(_configuration.Host, + _configuration.Port, _configuration.UseTls, + cancellationToken); + + await client.AuthenticateAsync(_configuration.Username, + _configuration.Password, cancellationToken); + + await client.SendAsync(message, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index 47ecef3..ccfa74f 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -2,6 +2,16 @@ "version": 1, "dependencies": { "net9.0": { + "MailKit": { + "type": "Direct", + "requested": "[4.12.1, )", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "Microsoft.Extensions.Http": { "type": "Direct", "requested": "[9.0.4, )", @@ -40,6 +50,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -289,11 +304,25 @@ "Microsoft.IdentityModel.Logging": "8.11.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "QuikGraph": { "type": "Transitive", "resolved": "2.5.0", "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -308,6 +337,11 @@ "resolved": "1.6.2", "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, "application": { "type": "Project", "dependencies": { diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 1a02d20..34e1b41 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -65,6 +65,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", @@ -87,6 +92,15 @@ "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" } }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -775,6 +789,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -848,6 +871,11 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -1012,6 +1040,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" } From 6a9504d6ff38f00193e496d5e6da6fdda2b1dd4a Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 18:02:02 +0300 Subject: [PATCH 31/35] add payment email notifications --- .../GetPaymentLink/GetPaymentLinkCommand.cs | 2 + .../GetPaymentLinkCommandHandler.cs | 56 +- .../GetPaymentLinkCommandValidator.cs | 9 + .../ProcessCallbackCommandHandler.cs | 241 ++- .../ViewModels/TicketGroupPaymentViewModel.cs | 2 + .../Resources/Localization/en-US.json | 14 +- .../SearchAll/SearchAllQueryHandler.cs | 12 +- .../Configuration/Configuration.cs | 5 +- src/Domain/Entities/TicketGroup.cs | 2 + src/Domain/Entities/VehicleEnrollment.cs | 14 +- src/Domain/Enums/Currency.cs | 15 + src/HttpApi/Controllers/PaymentController.cs | 1 + .../TicketGroupConfiguration.cs | 18 +- ...1846_Add_email_to_Ticket_Group.Designer.cs | 1339 +++++++++++++++++ ...0250529131846_Add_email_to_Ticket_Group.cs | 30 + .../PostgreSqlDbContextModelSnapshot.cs | 4 + 16 files changed, 1723 insertions(+), 41 deletions(-) create mode 100644 src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs index 98f3956..66f6ce8 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs @@ -17,6 +17,8 @@ public record GetPaymentLinkCommand : IRequest public DateOnly PassangerBirthDate { get; set; } + public string? PassangerEmail { get; set; } + public ICollection Tickets { get; set; } diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs index d5ac855..48b6e00 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -21,16 +21,27 @@ public class GetPaymentLinkCommandHandler : private readonly IStringLocalizer _localizer; + private readonly EmailSenderService _emailSender; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + private readonly SessionCultureService _sessionCultureService; + public GetPaymentLinkCommandHandler( UnitOfWork unitOfWork, CurrencyConverterService currencyConverterService, LiqPayPaymentService liqPayPaymentService, - IStringLocalizer localizer) + IStringLocalizer localizer, + EmailSenderService emailSender, + SessionTimeZoneService SessionTimeZoneService, + SessionCultureService sessionCultureService) { _unitOfWork = unitOfWork; _currencyConverterService = currencyConverterService; _liqPayPaymentService = liqPayPaymentService; _localizer = localizer; + _emailSender = emailSender; + _sessionTimeZoneService = SessionTimeZoneService; + _sessionCultureService = sessionCultureService; } public async Task Handle( @@ -336,7 +347,7 @@ public class GetPaymentLinkCommandHandler : var costToDeparture = verad - .TakeWhile(rad => rad.Id != departureRouteAddressId) + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) .Aggregate((decimal)0, (sum, next) => sum + next.CostToNextAddress); @@ -412,6 +423,7 @@ public class GetPaymentLinkCommandHandler : PurchaseTime = DateTimeOffset.UtcNow, Status = TicketStatus.Reserved, TravelTime = travelTime, + PassangerEmail = request.PassangerEmail, Tickets = request.Tickets.Select( t => { @@ -428,12 +440,6 @@ public class GetPaymentLinkCommandHandler : var detail = ticketsDetails .SingleOrDefault(td => td.order == t.Order); - var currency = Currency.UAH; - var cost = _currencyConverterService - .ConvertAsync( - detail.cost, detail.currency, currency, - cancellationToken).Result; - return new Ticket() { DepartureRouteAddressId = departureRouteAddress.Id, @@ -441,8 +447,8 @@ public class GetPaymentLinkCommandHandler : ArrivalRouteAddressId = arrivalRouteAddress.Id, ArrivalRouteAddress = arrivalRouteAddress, Order = t.Order, - Cost = cost, - Currency = currency, + Cost = detail.cost, + Currency = detail.currency, VehicleEnrollmentId = ve.Id }; }) @@ -456,7 +462,11 @@ public class GetPaymentLinkCommandHandler : _unitOfWork.Dispose(); - var amount = entity.Tickets.Sum(e => e.Cost); + var amount = entity.Tickets.Sum(e => + _currencyConverterService + .ConvertAsync( + e.Cost, e.Currency, Currency.UAH, + cancellationToken).Result); var guid = entity.Guid; var validity = TimeSpan.FromMinutes(10); var resultPath = request.ResultPath; @@ -465,9 +475,31 @@ public class GetPaymentLinkCommandHandler : var paymentLink = _liqPayPaymentService .GetPaymentLink( amount, Currency.UAH, guid.ToString(), validity, - _localizer["PaymentProcessing.TicketPaymentDescription"], + _localizer["PaymentProcessing.Ticket.PaymentDescription"], resultPath, callbackPath); + if (request.PassangerEmail != null) + { + var validUntil = DateTimeOffset.UtcNow + .Add(validity) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset); + + var subject = + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCreated.Subject"]; + + var body = String.Format( + _sessionCultureService.Culture, + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCreated.Body"], + Currency.UAH.Round(amount), Currency.UAH.Name, + validUntil, paymentLink); + + await _emailSender.SendAsync( + new[] { request.PassangerEmail }, subject, + body, cancellationToken); + } + return new PaymentLinkDto() { PaymentLink = paymentLink }; } } diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs index fa1f227..0662c2f 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs @@ -60,6 +60,15 @@ public class GetPaymentLinkCommandValidator : cultureService.Culture, localizer["FluentValidation.GreaterThanOrEqualTo"], DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + When(tg => tg.PassangerEmail != null, () => + { + RuleFor(v => v.PassangerEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + }); RuleFor(tg => tg.Tickets) .IsUnique(t => t.VehicleEnrollmentGuid) diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs index ade9098..4064da6 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -5,6 +5,8 @@ using cuqmbr.TravelGuide.Application.Common.Exceptions; using System.Text; using Newtonsoft.Json; using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Application.Payments.LiqPay .TicketGroups.Commands.ProcessCallback; @@ -13,20 +15,31 @@ public class ProcessCallbackCommandHandler : IRequestHandler { private readonly UnitOfWork _unitOfWork; + private readonly LiqPayPaymentService _liqPayPaymentService; + private readonly IStringLocalizer _localizer; + + private readonly EmailSenderService _emailSender; + public ProcessCallbackCommandHandler( UnitOfWork unitOfWork, - LiqPayPaymentService liqPayPaymentService) + LiqPayPaymentService liqPayPaymentService, + IStringLocalizer localizer, + EmailSenderService emailSender) { _unitOfWork = unitOfWork; _liqPayPaymentService = liqPayPaymentService; + _localizer = localizer; + _emailSender = emailSender; } public async Task Handle( ProcessCallbackCommand request, CancellationToken cancellationToken) { + // Validate signature. + var isSignatureValid = _liqPayPaymentService .IsValidSignature(request.Data, request.Signature); @@ -35,6 +48,9 @@ public class ProcessCallbackCommandHandler : throw new ForbiddenException(); } + + // Parse request data. + var dataBytes = Convert.FromBase64String(request.Data); var dataJson = Encoding.UTF8.GetString(dataBytes); @@ -42,9 +58,11 @@ public class ProcessCallbackCommandHandler : string status = data.status; + var ticketGroupGuid = Guid.Parse((string)data.order_id); var ticketGroup = await _unitOfWork.TicketGroupRepository - .GetOneAsync(e => e.Guid == ticketGroupGuid, cancellationToken); + .GetOneAsync(e => e.Guid == ticketGroupGuid, + e => e.Tickets, cancellationToken); if (ticketGroup == null || ticketGroup.Status == TicketStatus.Purchased) @@ -52,6 +70,9 @@ public class ProcessCallbackCommandHandler : throw new ForbiddenException(); } + + // Process callback status + if (status.Equals("error") || status.Equals("failure")) { await _unitOfWork.TicketGroupRepository @@ -59,12 +80,228 @@ public class ProcessCallbackCommandHandler : } else if (status.Equals("success")) { + // Update ticket status + ticketGroup.Status = TicketStatus.Purchased; await _unitOfWork.TicketGroupRepository .UpdateOneAsync(ticketGroup, cancellationToken); + + + // Hydrate ticket group + + var vehicleEnrollmentIds = + ticketGroup.Tickets.Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + foreach (var t in ticketGroup.Tickets) + { + t.VehicleEnrollment = vehicleEnrollments + .Single(ve => ve.Id == t.VehicleEnrollmentId); + } + + + // Send email + + if (ticketGroup.PassangerEmail != null) + { + var subject = + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCompleted.Subject"]; + + var ticketDetails = GetTicketDetails(ticketGroup); + + var body = String.Format( + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCompleted.Body"], + ticketDetails); + + await _emailSender.SendAsync( + new[] { ticketGroup.PassangerEmail }, subject, + body, cancellationToken); + } } + await _unitOfWork.SaveAsync(cancellationToken); _unitOfWork.Dispose(); } + + private string GetTicketDetails(TicketGroup ticketGroup) + { + var sb = new StringBuilder(); + + sb.AppendLine("General:"); + sb.AppendLine(); + sb.AppendLine($"Ticket uuid: {ticketGroup.Guid}"); + sb.AppendLine($"Purchase Time: {ticketGroup.PurchaseTime}"); + sb.AppendLine(); + + var departureRouteAddressId = + ticketGroup.Tickets.First().DepartureRouteAddressId; + var arrivalRouteAddressId = + ticketGroup.Tickets.Last().ArrivalRouteAddressId; + + var departureTime = + ticketGroup.Tickets.First() + .VehicleEnrollment.GetDepartureTime(departureRouteAddressId); + var arrivalTime = + ticketGroup.Tickets.Last() + .VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId); + + var departureAddress = + ticketGroup.Tickets.First() + .VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == departureRouteAddressId) + .Address; + var arrivalAddress = + ticketGroup.Tickets.Last() + .VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == arrivalRouteAddressId) + .Address; + + var departureAddressName = + $"{departureAddress.City.Region.Country.Name}, " + + $"{departureAddress.City.Region.Name}, " + + $"{departureAddress.City.Name}, " + + $"{departureAddress.Name}"; + var arrivalAddressName = + $"{arrivalAddress.City.Region.Country.Name}, " + + $"{arrivalAddress.City.Region.Name}, " + + $"{arrivalAddress.City.Name}, " + + $"{arrivalAddress.Name}"; + + sb.AppendLine($"Departure: {departureAddressName} at {departureTime}."); + sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}."); + sb.AppendLine(); + sb.AppendLine(); + + sb.AppendLine($"Passanger details:"); + sb.AppendLine(); + sb.AppendLine($"First Name: {ticketGroup.PassangerFirstName}."); + sb.AppendLine($"Last Name: {ticketGroup.PassangerLastName}."); + sb.AppendLine($"Patronymic: {ticketGroup.PassangerPatronymic}."); + sb.AppendLine($"Sex: {ticketGroup.PassangerSex}."); + sb.AppendLine($"Birth Date: {ticketGroup.PassangerBirthDate}."); + sb.AppendLine($"Email: {ticketGroup.PassangerEmail}."); + sb.AppendLine(); + sb.AppendLine(); + + + sb.AppendLine("Vehicle enrollments' details:"); + sb.AppendLine(); + + foreach (var t in ticketGroup.Tickets) + { + departureRouteAddressId = t.DepartureRouteAddressId; + arrivalRouteAddressId = t.ArrivalRouteAddressId; + + departureTime = + t.VehicleEnrollment.GetDepartureTime(departureRouteAddressId); + arrivalTime = + t.VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId); + + departureAddress = + t.VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == departureRouteAddressId) + .Address; + arrivalAddress = + t.VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == arrivalRouteAddressId) + .Address; + + departureAddressName = + $"{departureAddress.City.Region.Country.Name}, " + + $"{departureAddress.City.Region.Name}, " + + $"{departureAddress.City.Name}, " + + $"{departureAddress.Name}"; + arrivalAddressName = + $"{arrivalAddress.City.Region.Country.Name}, " + + $"{arrivalAddress.City.Region.Name}, " + + $"{arrivalAddress.City.Name}, " + + $"{arrivalAddress.Name}"; + + var vehicle = t.VehicleEnrollment.Vehicle; + var company = vehicle.Company; + + sb.AppendLine($"Departure: {departureAddressName} at {departureTime}."); + sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}."); + + if (vehicle is Bus) + { + sb.AppendLine($"Vehicle: Bus, {((Bus)vehicle).Model}, " + + $"{((Bus)vehicle).Number}."); + } + else if (vehicle is Aircraft) + { + sb.AppendLine($"Vehicle: Aircraft, {((Aircraft)vehicle).Model}, " + + $"{((Aircraft)vehicle).Number}."); + } + else if (vehicle is Train) + { + sb.AppendLine($"Vehicle: Train, {((Train)vehicle).Model}, " + + $"{((Train)vehicle).Number}."); + } + else + { + throw new NotImplementedException(); + } + + sb.AppendLine($"Company: {company.Name}, ({company.ContactEmail}, " + + $"{company.ContactPhoneNumber})."); + + var cost = t.Currency.Round( + t.VehicleEnrollment + .GetCost(departureRouteAddressId,arrivalRouteAddressId)); + sb.AppendLine($"Cost: {cost} {t.Currency.Name}"); + sb.AppendLine(); + } + + return sb.ToString(); + } } diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs index 4d6d83b..0e4f447 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs @@ -13,6 +13,8 @@ public sealed class TicketGroupPaymentViewModel public DateOnly PassangerBirthDate { get; set; } + public string? PassangerEmail { get; set; } + public ICollection Tickets { get; set; } diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 832562f..af06306 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -62,6 +62,18 @@ } }, "PaymentProcessing": { - "TicketPaymentDescription": "Ticket purchase." + "Ticket": { + "PaymentDescription": "Ticket purchase.", + "Email": { + "PaymentCreated": { + "Subject": "Ticket purchase payment link.", + "Body": "You have reserved a ticket. Payment amount is {0} {1} Payment link is valid until {2}.\n\nLink: {3}" + }, + "PaymentCompleted": { + "Subject": "Ticket purchase complete.", + "Body": "Payment is succeeded.\n\n\nTicket details:\n\n{0}" + } + } + } } } diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs index ee286ee..02d83a3 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -357,16 +357,6 @@ public class SearchAllQueryHandler : tag = path.Select(e => e.Tag).Last(); - lastRouteAddressGuid = vehicleEnrollments - .Single(e => e.Id == tag.VehicleEnrollmentId) - .RouteAddressDetails - .Select(e => e.RouteAddress) - .OrderBy(e => e.Order) - .SkipWhile(e => e.Order != tag.RouteAddress.Order) - .Take(2) - .ElementAt(1) - .Guid; - costToNextAddress = await _currencyConverterService .ConvertAsync(tag.CostToNextAddress, tag.VehicleEnrollment.Currency, @@ -388,7 +378,7 @@ public class SearchAllQueryHandler : CostToNextAddress = 0, CurrentAddressStopTime = tag.CurrentAddressStopTime, Order = addressOrder, - RouteAddressUuid = lastRouteAddressGuid + RouteAddressUuid = tag.RouteAddress.Guid }); diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index d4b2ecc..7264d13 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -25,8 +25,9 @@ public static class Configuration .AddCommandLine(args) .Build(); - services.AddOptions() - .Bind(configuration); + services.AddOptions().Bind( + configuration.GetSection( + PersistenceConfigurationOptions.SectionName)); services.AddOptions() .Bind(configuration); diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs index c40a2ae..b532b78 100644 --- a/src/Domain/Entities/TicketGroup.cs +++ b/src/Domain/Entities/TicketGroup.cs @@ -14,6 +14,8 @@ public sealed class TicketGroup : EntityBase public DateOnly PassangerBirthDate { get; set; } + public string? PassangerEmail { get; set; } + public DateTimeOffset PurchaseTime { get; set; } public TicketStatus Status { get; set; } diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs index 7cfdc46..21470de 100644 --- a/src/Domain/Entities/VehicleEnrollment.cs +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -121,7 +121,7 @@ public class VehicleEnrollment : EntityBase .OrderBy(e => e.RouteAddress.Order); var departureRouteAddressDetail = orderedRouteAddressDetails - .Single(e => e.Id == DepartureRouteAddressId); + .Single(e => e.RouteAddressId == DepartureRouteAddressId); var timeInStops = TimeSpan.Zero; foreach (var routeAddressDetail in orderedRouteAddressDetails) @@ -159,8 +159,8 @@ public class VehicleEnrollment : EntityBase return RouteAddressDetails .OrderBy(e => e.RouteAddress.Order) - .SkipWhile(e => e.Id != DepartureRouteAddressId) - .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) .Count() - 1; } @@ -180,8 +180,8 @@ public class VehicleEnrollment : EntityBase return RouteAddressDetails .OrderBy(e => e.RouteAddress.Order) - .SkipWhile(e => e.Id != DepartureRouteAddressId) - .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeToNextAddress); } @@ -202,8 +202,8 @@ public class VehicleEnrollment : EntityBase return RouteAddressDetails .OrderBy(e => e.RouteAddress.Order) - .SkipWhile(e => e.Id != DepartureRouteAddressId) - .TakeWhile(e => e.Id != ArrivalRouteAddressId) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) .Aggregate((decimal)0, (sum, next) => sum += next.CostToNextAddress); } diff --git a/src/Domain/Enums/Currency.cs b/src/Domain/Enums/Currency.cs index fb5f312..cf30549 100644 --- a/src/Domain/Enums/Currency.cs +++ b/src/Domain/Enums/Currency.cs @@ -14,24 +14,39 @@ public abstract class Currency : Enumeration protected Currency(int value, string name) : base(value, name) { } + protected virtual byte DecimalDigits { get; } = byte.MaxValue; + + public decimal Round(decimal amount) + { + return Math.Round(amount, DecimalDigits); + } + // When no currency is specified private sealed class DefaultCurrency : Currency { public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { } + + protected override byte DecimalDigits => 2; } private sealed class USDCurrency : Currency { public USDCurrency() : base(840, "USD") { } + + protected override byte DecimalDigits => 2; } private sealed class EURCurrency : Currency { public EURCurrency() : base(978, "EUR") { } + + protected override byte DecimalDigits => 2; } private sealed class UAHCurrency : Currency { public UAHCurrency() : base(980, "UAH") { } + + protected override byte DecimalDigits => 2; } } diff --git a/src/HttpApi/Controllers/PaymentController.cs b/src/HttpApi/Controllers/PaymentController.cs index b7c2e70..04c28e8 100644 --- a/src/HttpApi/Controllers/PaymentController.cs +++ b/src/HttpApi/Controllers/PaymentController.cs @@ -48,6 +48,7 @@ public class PaymentController : ControllerBase PassangerPatronymic = viewModel.PassangerPatronymic, PassangerSex = Sex.FromName(viewModel.PassangerSex), PassangerBirthDate = viewModel.PassangerBirthDate, + PassangerEmail = viewModel.PassangerEmail, Tickets = viewModel.Tickets.Select(e => new TicketGroupPaymentTicketModel() { diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs index 10e4a65..017bbd4 100644 --- a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -49,37 +49,43 @@ public class TicketGroupConfiguration : BaseConfiguration builder - .Property(a => a.PassangerFirstName) + .Property(tg => tg.PassangerFirstName) .HasColumnName("passanger_first_name") .HasColumnType("varchar(32)") .IsRequired(true); builder - .Property(a => a.PassangerLastName) + .Property(tg => tg.PassangerLastName) .HasColumnName("passanger_last_name") .HasColumnType("varchar(32)") .IsRequired(true); builder - .Property(a => a.PassangerPatronymic) + .Property(tg => tg.PassangerPatronymic) .HasColumnName("passanger_patronymic") .HasColumnType("varchar(32)") .IsRequired(true); builder - .Property(a => a.PassangerBirthDate) + .Property(tg => tg.PassangerBirthDate) .HasColumnName("passanger_birth_date") .HasColumnType("date") .IsRequired(true); builder - .Property(a => a.PurchaseTime) + .Property(tg => tg.PassangerEmail) + .HasColumnName("passanger_email") + .HasColumnType("varchar(256)") + .IsRequired(false); + + builder + .Property(tg => tg.PurchaseTime) .HasColumnName("purchase_time") .HasColumnType("timestamptz") .IsRequired(true); builder - .Property(a => a.TravelTime) + .Property(tg => tg.TravelTime) .HasColumnName("travel_time") .HasColumnType("interval") .IsRequired(true); diff --git a/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs new file mode 100644 index 0000000..9156d66 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs @@ -0,0 +1,1339 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250529131846_Add_email_to_Ticket_Group")] + partial class Add_email_to_Ticket_Group + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs new file mode 100644 index 0000000..836bffb --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_email_to_Ticket_Group : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "passanger_email", + schema: "application", + table: "ticket_groups", + type: "varchar(256)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "passanger_email", + schema: "application", + table: "ticket_groups"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 93002e6..99901fb 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -737,6 +737,10 @@ namespace Persistence.PostgreSql.Migrations .HasColumnType("date") .HasColumnName("passanger_birth_date"); + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + b.Property("PassangerFirstName") .IsRequired() .HasColumnType("varchar(32)") From e5b318022057f7ed8a7df0f85052ff9b9bc748ae Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 18:02:26 +0300 Subject: [PATCH 32/35] update uk-UA localization --- .../Resources/Localization/uk-UA.json | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/src/Application/Resources/Localization/uk-UA.json b/src/Application/Resources/Localization/uk-UA.json index 740082d..7c98d20 100644 --- a/src/Application/Resources/Localization/uk-UA.json +++ b/src/Application/Resources/Localization/uk-UA.json @@ -1,46 +1,79 @@ { "FluentValidation": { - "MaximumLength": "Повинно бути менше ніж {0:G} символів.", "NotEmpty": "Не повинно бути порожнім.", - "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0:G}.", - "LessThanOrEqualTo": "Повинно бути менше або дорівнювати {0:G}." + "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0}.", + "LessThanOrEqualTo": "Повинно бути менше або дорівнювати {0}.", + "MinimumLength": "Довжина повинна бути більшою або дорівнювати {0} символам.", + "MaximumLength": "Довжина повинна бути меншою або дорівнювати {0} символам.", + "MustBeInEnum": "Повинно бути одним із наступних: {0}.", + "IsUsername": "Може містити латинські літери у нижньому регістрі (a-z), цифри (0-9), дефіси (-), підкреслення (_) та крапки (.).", + "IsEmail": "Повинно бути дійсною електронною адресою відповідно до RFC 5321.", + "IsPhoneNumber": "Повинно бути дійсним номером телефону відповідно до ITU-T E.164 без роздільних символів.", + "IsUnique": "Елементи колекції повинні бути унікальними." + }, + "Validation": { + "DistinctOrder": "Повинно мати унікальні значення порядку.", + "SameVehicleType": "Повинно мати однаковий тип транспортного засобу.", + "DateTimeOffset": { + "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0:U}" + }, + "VehicleEnrollments": { + "OverlapWithOther": "Наданий запис транспортного засобу перетинається за розкладом з іншим.", + "NegativeTime": "Вказаний час повинен бути додатнім проміжком часу.", + "NegativeCost": "Вказана вартість повинна бути додатнім значенням." + } }, "ExceptionHandling": { "ValidationException": { - "Title": "Виникла одна або декілька помилок валідації.", - "Detail": "Надані дані не задовольняють вимогам валідації." + "Title": "Виникла одна або кілька помилок валідації.", + "Detail": "Надані дані не відповідають вимогам валідації." }, "RegistrationException": { "Title": "Реєстрація не вдалася.", "Detail": "Електронна пошта вже зареєстрована." }, "UnAuthorizedException": { - "Title": "Доступ без автентифікації заблоковано.", + "Title": "Неавтентифікований доступ заблоковано.", "Detail": "Запит не містить дійсних автентифікаційних даних для цільового ресурсу." }, - "AithenticationException": { + "AuthenticationException": { "Title": "Автентифікація не вдалася.", "Detail": "Перевірте правильність наданих облікових даних." }, "LoginException": { "Title": "Вхід не вдалий.", - "Detail": "Надані електронна пошта та/або пароль недійсні." + "Detail": "Надана електронна пошта та/або пароль недійсні." }, "ForbiddenException": { - "Title": "Доступ заборонено.", + "Title": "Неавторизований доступ заблоковано.", "Detail": "У вас недостатньо прав для виконання запиту." }, "DuplicateEntityException": { - "Title": "Об’єкт вже існує.", - "Detail": "Дублювання не дозволяється." + "Title": "Об'єкт вже існує.", + "Detail": "Дублікати заборонені." }, "NotFoundException": { - "Title": "Один або декілька ресурсів не знайдено.", + "Title": "Один або кілька ресурсів не знайдено.", "Detail": "Перевірте правильність вхідних даних." }, "UnhandledException": { - "Title": "Виникла одна або декілька внутрішніх помилок сервера.", - "Detail": "Повідомте про цю помилку службі підтримки сервісу." + "Title": "Виникла одна або кілька внутрішніх помилок сервера.", + "Detail": "Повідомте про цю помилку команді підтримки сервісу." + } + }, + "PaymentProcessing": { + "Ticket": { + "PaymentDescription": "Придбання квитка.", + "Email": { + "PaymentCreated": { + "Subject": "Посилання для оплати придбання квитка.", + "Body": "Ви забронювали квиток. Сума оплати становить {0} {1}. Посилання дійсне до {2}.\n\nПосилання: {3}" + }, + "PaymentCompleted": { + "Subject": "Придбання квитка завершено.", + "Body": "Оплата пройшла успішно.\n\n\nОсь деталі вашого квитка:\n\n{0}" + } + } } } } From 4d1f6edc2e763bc4f48693e9ae9e1434af58107e Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 30 May 2025 13:14:20 +0300 Subject: [PATCH 33/35] add ticket group queries --- .../GetPaymentLinkCommandHandler.cs | 2 +- .../AddTicketGroupCommandHandler.cs | 4 +- .../GetTicketGroup/GetTicketGroupQuery.cs | 8 + .../GetTicketGroupQueryAuthorizer.cs | 31 +++ .../GetTicketGroupQueryHandler.cs | 175 +++++++++++++ .../GetTicketGroupQueryValidator.cs | 14 ++ .../GetTicketGroupsPageQuery.cs | 53 ++++ .../GetTicketGroupsPageQueryAuthorizer.cs | 31 +++ .../GetTicketGroupsPageQueryHandler.cs | 232 ++++++++++++++++++ .../GetTicketGroupsPageQueryValidator.cs | 80 ++++++ .../TicketGroups/TicketAddressDto.cs | 53 ---- src/Application/TicketGroups/TicketDto.cs | 50 ---- .../TicketGroups/TicketGroupAddressDto.cs | 87 +++++++ .../TicketGroups/TicketGroupCompanyDto.cs | 25 ++ .../TicketGroups/TicketGroupDto.cs | 123 +++++++++- .../TicketGroups/TicketGroupVehicleDto.cs | 95 +++++++ .../TicketGroupVehicleEnrollmentDto.cs | 99 ++++++++ .../GetTicketGroupsPageFilterViewModel.cs | 18 +- .../ViewModels/UpdateTicketGroupViewModel.cs | 21 -- .../Controllers/TicketGroupsController.cs | 231 +++++++---------- 20 files changed, 1152 insertions(+), 280 deletions(-) create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs create mode 100644 src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs delete mode 100644 src/Application/TicketGroups/TicketAddressDto.cs delete mode 100644 src/Application/TicketGroups/TicketDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupAddressDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupCompanyDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupVehicleDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs delete mode 100644 src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs index 48b6e00..dcdd08f 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -333,7 +333,7 @@ public class GetPaymentLinkCommandHandler : // TODO: This counts departure address stop time which is // not wrong but may be not desired. var timeToDeparture = verad - .TakeWhile(rad => rad.Id != departureRouteAddressId) + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) .Aggregate(TimeSpan.Zero, (sum, next) => sum + next.TimeToNextAddress + next.CurrentAddressStopTime); diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs index d3edfc0..e6f0c3a 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -325,7 +325,7 @@ public class AddTicketGroupCommandHandler : // TODO: This counts departure address stop time which is // not wrong but may be not desired. var timeToDeparture = verad - .TakeWhile(rad => rad.Id != departureRouteAddressId) + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) .Aggregate(TimeSpan.Zero, (sum, next) => sum + next.TimeToNextAddress + next.CurrentAddressStopTime); @@ -339,7 +339,7 @@ public class AddTicketGroupCommandHandler : var costToDeparture = verad - .TakeWhile(rad => rad.Id != departureRouteAddressId) + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) .Aggregate((decimal)0, (sum, next) => sum + next.CostToNextAddress); diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs new file mode 100644 index 0000000..b06e83a --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public record GetTicketGroupQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs new file mode 100644 index 0000000..4305de1 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTicketGroupQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTicketGroupQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs new file mode 100644 index 0000000..f3c270c --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs @@ -0,0 +1,175 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly CurrencyConverterService _currencyConverter; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + + private readonly object _lock = new(); + + public GetTicketGroupQueryHandler(UnitOfWork unitOfWork, + IMapper mapper, CurrencyConverterService currencyConverterService, + SessionCurrencyService sessionCurrencyService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _currencyConverter = currencyConverterService; + _sessionCurrencyService = sessionCurrencyService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task Handle( + GetTicketGroupQuery request, + CancellationToken cancellationToken) + { + var ticketGroup = await _unitOfWork.TicketGroupRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Tickets, + cancellationToken); + + if (ticketGroup == null) + { + throw new NotFoundException(); + } + + + // Hydrate + + var vehicleEnrollmentIds = + ticketGroup.Tickets.Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + + // TODO: Replace with AutoMapper resolvers + // Convert currency and apply session time zone + + var convertTasks = new List(); + + foreach (var t in ticketGroup.Tickets) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + t.VehicleEnrollment.DepartureTime = + TimeZoneInfo.ConvertTime(t.VehicleEnrollment.DepartureTime, + _sessionTimeZoneService.TimeZone); + })); + + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync(t.Cost, + t.Currency, _sessionCurrencyService.Currency, + cancellationToken) + .Result; + + t.Cost = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + + foreach (var rad in t.VehicleEnrollment.RouteAddressDetails) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync( + rad.CostToNextAddress, t.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken) + .Result; + + rad.CostToNextAddress = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + } + } + + Task.WaitAll(convertTasks); + + foreach (var t in ticketGroup.Tickets) + { + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + t.Currency = _sessionCurrencyService.Currency; + t.VehicleEnrollment.Currency = _sessionCurrencyService.Currency; + } + + + _unitOfWork.Dispose(); + + var dto = _mapper.Map(ticketGroup); + + dto.Currency = _sessionCurrencyService.Currency.Name; + + return dto; + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs new file mode 100644 index 0000000..937166a --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryValidator : AbstractValidator +{ + public GetTicketGroupQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs new file mode 100644 index 0000000..2d0a9f9 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs @@ -0,0 +1,53 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public record GetTicketGroupsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public HashSet? PassangerSex { get; set; } + + public DateOnly? PassangerBirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? PassangerBirthDateLessThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeLessThanOrEqualTo { get; set; } + + public HashSet? Statuses { get; set; } + + public HashSet? VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + // TODO: Add filtering parametetrs listed below. It is hard to + // be done because of pagination. + + // public decimal? CostGreaterThanOrEqualTo { get; set; } + // + // public decimal? CostLessThanOrEqualTo { get; set; } + // + // public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + // + // public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + // + // public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + // + // public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + // + // public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + // + // public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs new file mode 100644 index 0000000..ba67439 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTicketGroupsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTicketGroupsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs new file mode 100644 index 0000000..a7c0e2b --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs @@ -0,0 +1,232 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly CurrencyConverterService _currencyConverter; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + + private readonly object _lock = new(); + + public GetTicketGroupsPageQueryHandler(UnitOfWork unitOfWork, + IMapper mapper, CurrencyConverterService currencyConverterService, + SessionCurrencyService sessionCurrencyService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _currencyConverter = currencyConverterService; + _sessionCurrencyService = sessionCurrencyService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task> Handle( + GetTicketGroupsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.TicketGroupRepository.GetPageAsync( + e => + (e.PassangerFirstName.ToLower().Contains(request.Search.ToLower()) || + e.PassangerLastName.ToLower().Contains(request.Search.ToLower()) || + e.PassangerPatronymic.ToLower().Contains(request.Search.ToLower()) || + (e.PassangerEmail != null ? + e.PassangerEmail.ToLower().Contains(request.Search.ToLower()) : + false)) && + (request.PassangerSex != null + ? request.PassangerSex.Contains(e.PassangerSex) + : true) && + (request.PassangerBirthDateGreaterThanOrEqualTo != null + ? e.PassangerBirthDate >= request.PassangerBirthDateGreaterThanOrEqualTo + : true) && + (request.PassangerBirthDateLessThanOrEqualTo != null + ? e.PassangerBirthDate <= request.PassangerBirthDateLessThanOrEqualTo + : true) && + (request.PurchaseTimeGreaterThanOrEqualTo != null + ? e.PurchaseTime >= request.PurchaseTimeGreaterThanOrEqualTo + : true) && + (request.PurchaseTimeLessThanOrEqualTo != null + ? e.PurchaseTime <= request.PurchaseTimeLessThanOrEqualTo + : true) && + (request.PassangerSex != null + ? request.PassangerSex.Contains(e.PassangerSex) + : true) && + (request.Statuses != null + ? request.Statuses.Contains(e.Status) + : true) && + (request.VehicleTypes != null + ? e.Tickets + .Select(t => t.VehicleEnrollment.Vehicle.VehicleType) + .Any(vt => request.VehicleTypes.Contains(vt)) + : true) && + (request.TravelTimeGreaterThanOrEqualTo != null + ? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo + : true) && + (request.TravelTimeLessThanOrEqualTo != null + ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true), + e => e.Tickets, + request.PageNumber, request.PageSize, cancellationToken); + + var ticketGroups = paginatedList.Items; + + + // Hydrate + + var vehicleEnrollmentIds = + ticketGroups.SelectMany(tg => tg.Tickets) + .Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + + // TODO: Replace with AutoMapper resolvers + // Convert currency and apply session time zone + + var convertTasks = new List(); + var processedRouteAddressDetailIds = new HashSet(); + + foreach (var t in ticketGroups.SelectMany(tg => tg.Tickets)) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + t.VehicleEnrollment.DepartureTime = + TimeZoneInfo.ConvertTime(t.VehicleEnrollment.DepartureTime, + _sessionTimeZoneService.TimeZone); + })); + + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync(t.Cost, + t.Currency, _sessionCurrencyService.Currency, + t.TicketGroup.PurchaseTime, cancellationToken) + .Result; + + t.Cost = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + + foreach (var rad in t.VehicleEnrollment.RouteAddressDetails) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + if (processedRouteAddressDetailIds.Contains(rad.Id)) + { + return; + } + + var convertedCost = _currencyConverter.ConvertAsync( + rad.CostToNextAddress, t.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + t.TicketGroup.PurchaseTime, cancellationToken) + .Result; + + rad.CostToNextAddress = _sessionCurrencyService + .Currency.Round(convertedCost); + + processedRouteAddressDetailIds.Add(rad.Id); + } + })); + } + } + + Task.WaitAll(convertTasks); + + foreach (var t in ticketGroups.SelectMany(tg => tg.Tickets)) + { + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + t.Currency = _sessionCurrencyService.Currency; + t.VehicleEnrollment.Currency = _sessionCurrencyService.Currency; + } + + + var mappedItems = + _mapper.Map>(ticketGroups); + + foreach (var item in mappedItems) + { + item.Currency = _sessionCurrencyService.Currency.Name; + } + + mappedItems = QueryableExtension + .ApplySort(mappedItems.AsQueryable(), request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs new file mode 100644 index 0000000..ed16b23 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs @@ -0,0 +1,80 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryValidator : AbstractValidator +{ + public GetTicketGroupsPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + When(v => v.PassangerSex != null, () => + { + RuleForEach(v => v.PassangerSex) + .Must((v, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + }); + + When(v => v.Statuses != null, () => + { + RuleForEach(v => v.Statuses) + .Must((v, s) => TicketStatus.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + TicketStatus.Enumerations.Values.Select(e => e.Name)))); + }); + + When(v => v.VehicleTypes != null, () => + { + RuleForEach(v => v.VehicleTypes) + .Must((v, s) => VehicleType.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + }); + } +} diff --git a/src/Application/TicketGroups/TicketAddressDto.cs b/src/Application/TicketGroups/TicketAddressDto.cs deleted file mode 100644 index d62e5c2..0000000 --- a/src/Application/TicketGroups/TicketAddressDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Mappings; -using cuqmbr.TravelGuide.Domain.Entities; - -namespace cuqmbr.TravelGuide.Application.TicketGroups; - -public sealed class TicketAddressDto : IMapFrom
-{ - public Guid Uuid { get; set; } - - public string Name { get; set; } - - public double Longitude { get; set; } - - public double Latitude { get; set; } - - public Guid CountryUuid { get; set; } - - public string CountryName { get; set; } - - public Guid RegionUuid { get; set; } - - public string RegionName { get; set; } - - public Guid CityUuid { get; set; } - - public string CityName { get; set; } - - public void Mapping(MappingProfile profile) - { - profile.CreateMap() - .ForMember( - d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)) - .ForMember( - d => d.CountryUuid, - opt => opt.MapFrom(s => s.City.Region.Country.Guid)) - .ForMember( - d => d.CountryName, - opt => opt.MapFrom(s => s.City.Region.Country.Name)) - .ForMember( - d => d.RegionUuid, - opt => opt.MapFrom(s => s.City.Region.Guid)) - .ForMember( - d => d.RegionName, - opt => opt.MapFrom(s => s.City.Region.Name)) - .ForMember( - d => d.CityUuid, - opt => opt.MapFrom(s => s.City.Guid)) - .ForMember( - d => d.CityName, - opt => opt.MapFrom(s => s.City.Name)); - } -} diff --git a/src/Application/TicketGroups/TicketDto.cs b/src/Application/TicketGroups/TicketDto.cs deleted file mode 100644 index 93194a5..0000000 --- a/src/Application/TicketGroups/TicketDto.cs +++ /dev/null @@ -1,50 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Mappings; -using cuqmbr.TravelGuide.Domain.Entities; - -namespace cuqmbr.TravelGuide.Application.TicketGroups; - -public sealed class TicketDto : IMapFrom -{ - public Guid Uuid { get; set; } - - public Guid DepartureRouteAddressUuid { get; set; } - - public Guid ArrivalRouteAddressUuid { get; set; } - - public TicketAddressDto DepartureAddress { get; set; } - - public TicketAddressDto ArrivalAddress { get; set; } - - public short Order { get; set; } - - public Guid VehicleEnrollmentUuid { get; set; } - - // TODO: Add VehicleEnrollment model - - public string Currency { get; set; } - - public decimal Cost { get; set; } - - public void Mapping(MappingProfile profile) - { - profile.CreateMap() - .ForMember( - d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)) - .ForMember( - d => d.DepartureRouteAddressUuid, - opt => opt.MapFrom(s => s.DepartureRouteAddress.Guid)) - .ForMember( - d => d.ArrivalRouteAddressUuid, - opt => opt.MapFrom(s => s.ArrivalRouteAddress.Guid)) - .ForMember( - d => d.DepartureAddress, - opt => opt.MapFrom(s => s.DepartureRouteAddress.Address)) - .ForMember( - d => d.ArrivalAddress, - opt => opt.MapFrom(s => s.ArrivalRouteAddress.Address)) - .ForMember( - d => d.VehicleEnrollmentUuid, - opt => opt.MapFrom(s => s.VehicleEnrollment.Guid)); - } -} diff --git a/src/Application/TicketGroups/TicketGroupAddressDto.cs b/src/Application/TicketGroups/TicketGroupAddressDto.cs new file mode 100644 index 0000000..dd86935 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupAddressDto.cs @@ -0,0 +1,87 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupAddressDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public short Order { get; set; } + + public Guid RouteAddressUuid { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.Guid)) + .ForMember( + d => d.Name, + opt => opt.MapFrom(s => s.RouteAddress.Address.Name)) + .ForMember( + d => d.Longitude, + opt => opt.MapFrom(s => s.RouteAddress.Address.Longitude)) + .ForMember( + d => d.Latitude, + opt => opt.MapFrom(s => s.RouteAddress.Address.Latitude)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Name)) + .ForMember( + d => d.TimeToNextAddress, + opt => opt.MapFrom(s => s.TimeToNextAddress)) + .ForMember( + d => d.CostToNextAddress, + opt => opt.MapFrom(s => s.CostToNextAddress)) + .ForMember( + d => d.CurrentAddressStopTime, + opt => opt.MapFrom(s => s.CurrentAddressStopTime)) + .ForMember( + d => d.Order, + opt => opt.MapFrom(s => s.RouteAddress.Order)) + .ForMember( + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.RouteAddress.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupCompanyDto.cs b/src/Application/TicketGroups/TicketGroupCompanyDto.cs new file mode 100644 index 0000000..d0e4163 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupCompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupCompanyDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupDto.cs b/src/Application/TicketGroups/TicketGroupDto.cs index ae9e0e8..6c5b3c3 100644 --- a/src/Application/TicketGroups/TicketGroupDto.cs +++ b/src/Application/TicketGroups/TicketGroupDto.cs @@ -18,14 +18,29 @@ public sealed class TicketGroupDto : IMapFrom public DateOnly PassangerBirthDate { get; set; } + public string? PassangerEmail { get; set; } + public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public string Status { get; set; } + + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } public TimeSpan TravelTime { get; set; } + public TimeSpan TimeInStops { get; set; } - public ICollection Tickets { get; set; } + public int NumberOfTransfers { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + + public ICollection Enrollments { get; set; } public void Mapping(MappingProfile profile) { @@ -40,6 +55,108 @@ public sealed class TicketGroupDto : IMapFrom d => d.PurchaseTime, opt => opt .MapFrom( - s => s.PurchaseTime)); + s => s.PurchaseTime)) + .ForMember( + d => d.Status, + opt => opt.MapFrom(s => s.Status.Name)) + .ForMember( + d => d.DepartureTime, + opt => opt.MapFrom( + (s, d) => + { + var departureRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .DepartureRouteAddressId; + return + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetDepartureTime(departureRouteAddressId); + })) + .ForMember( + d => d.ArrivalTime, + opt => opt.MapFrom( + (s, d) => + { + var arrivalRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .ArrivalRouteAddressId; + return + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetArrivalTime(arrivalRouteAddressId); + })) + .ForMember( + d => d.TravelTime, + opt => opt.MapFrom( + (s, d) => + { + var departureRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .DepartureRouteAddressId; + var arrivalRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .ArrivalRouteAddressId; + var departureTime = + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetDepartureTime(departureRouteAddressId); + var arrivalTime = + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetArrivalTime(departureRouteAddressId); + return arrivalTime - departureTime; + })) + .ForMember( + d => d.TimeInStops, + opt => opt.MapFrom( + (s, d) => + { + var timePeriodsInStops = + s.Tickets.Select(t => + { + var departureRouteAddressId = + t.DepartureRouteAddressId; + var arrivalRouteAddressId = + t.ArrivalRouteAddressId; + return + t.VehicleEnrollment.GetTimeInStops( + departureRouteAddressId, + arrivalRouteAddressId); + }); + return + timePeriodsInStops + .Aggregate(TimeSpan.Zero, + (sum, next) => sum += next); + })) + .ForMember( + d => d.NumberOfTransfers, + opt => opt.MapFrom(s => s.Tickets.Count() - 1)) + .ForMember( + d => d.Cost, + opt => opt.MapFrom( + (s, d) => + { + var costs = + s.Tickets.Select(t => t.Currency.Round(t.Cost)); + return + costs + .Aggregate((decimal)0, + (sum, next) => sum += next); + })) + .ForMember( + d => d.Enrollments, + opt => opt.MapFrom(s => s.Tickets)); } } diff --git a/src/Application/TicketGroups/TicketGroupVehicleDto.cs b/src/Application/TicketGroups/TicketGroupVehicleDto.cs new file mode 100644 index 0000000..006d245 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupVehicleDto.cs @@ -0,0 +1,95 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} + diff --git a/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs b/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs new file mode 100644 index 0000000..ac1ac64 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs @@ -0,0 +1,99 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupVehicleEnrollmentDto : IMapFrom +{ + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeMoving { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfStops { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public Guid Uuid { get; set; } + + public short Order { get; set; } + + public TicketGroupCompanyDto Company { get; set; } + + public TicketGroupVehicleDto Vehicle { get; set; } + + public ICollection Addresses { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.VehicleEnrollment.Guid)) + .ForMember( + d => d.DepartureTime, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment + .GetDepartureTime(s.DepartureRouteAddressId); + })) + .ForMember( + d => d.ArrivalTime, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment + .GetArrivalTime(s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.TravelTime, + opt => opt.MapFrom( + (s, d) => + { + var departureTime = s.VehicleEnrollment + .GetDepartureTime(s.DepartureRouteAddressId); + var arrivalTime = s.VehicleEnrollment + .GetArrivalTime(s.ArrivalRouteAddressId); + return arrivalTime - departureTime; + })) + .ForMember( + d => d.TimeInStops, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment.GetTimeInStops( + s.DepartureRouteAddressId, s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.NumberOfStops, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment.GetNumberOfStops( + s.DepartureRouteAddressId, s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.Currency, + opt => opt.MapFrom(s => s.Currency)) + .ForMember( + d => d.Cost, + opt => opt.MapFrom(s => s.Currency.Round(s.Cost))) + .ForMember( + d => d.Company, + opt => opt.MapFrom(s => s.VehicleEnrollment.Vehicle.Company)) + .ForMember( + d => d.Vehicle, + opt => opt.MapFrom(s => s.VehicleEnrollment.Vehicle)) + .ForMember( + d => d.Addresses, + opt => opt.MapFrom(s => s.VehicleEnrollment.RouteAddressDetails)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs index 82399f3..cee2f12 100644 --- a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs +++ b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs @@ -2,11 +2,21 @@ namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; public sealed class GetTicketGroupsPageFilterViewModel { - public string? Sex { get; set; } + public HashSet? PassangerSex { get; set; } - public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + public DateOnly? PassangerBirthDateGreaterThanOrEqualTo { get; set; } - public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + public DateOnly? PassangerBirthDateLessThanOrEqualTo { get; set; } - public Guid? CompanyUuid { get; set; } + public DateTimeOffset? PurchaseTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeLessThanOrEqualTo { get; set; } + + public HashSet? Statuses { get; set; } + + public HashSet? VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } } diff --git a/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs deleted file mode 100644 index b58310e..0000000 --- a/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; - -public sealed class UpdateTicketGroupViewModel -{ - public string PassangerFirstName { get; set; } - - public string PassangerLastName { get; set; } - - public string PassangerPatronymic { get; set; } - - public string PassangerSex { get; set; } - - public DateOnly PassangerBirthDate { get; set; } - - public DateTimeOffset PurchaseTime { get; set; } - - public bool Returned { get; set; } - - - public ICollection Tickets { get; set; } -} diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs index adf8e3d..fff0d39 100644 --- a/src/HttpApi/Controllers/TicketGroupsController.cs +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -3,8 +3,12 @@ using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.TicketGroups; using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; +using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; +using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; using cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; using cuqmbr.TravelGuide.Application.TicketGroups.Models; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -12,7 +16,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TicketGroupsController : ControllerBase { [HttpPost] - [SwaggerOperation("Add a ticketGroup")] + [SwaggerOperation("Add a ticket group")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(TicketGroupDto))] @@ -67,149 +71,84 @@ public class TicketGroupsController : ControllerBase cancellationToken)); } - // [HttpGet] - // [SwaggerOperation("Get a list of all ticketGroups")] - // [SwaggerResponse( - // StatusCodes.Status200OK, "Request successful", - // typeof(PaginatedList))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task> GetPage( - // [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, - // [FromQuery] SortQuery sortQuery, - // [FromQuery] GetTicketGroupsPageFilterViewModel filterQuery, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send( - // new GetTicketGroupsPageQuery() - // { - // PageNumber = pageQuery.PageNumber, - // PageSize = pageQuery.PageSize, - // Search = searchQuery.Search, - // Sort = sortQuery.Sort, - // LongitudeGreaterOrEqualThan = - // filterQuery.LongitudeGreaterOrEqualThan, - // LongitudeLessOrEqualThan = - // filterQuery.LongitudeLessOrEqualThan, - // LatitudeGreaterOrEqualThan = - // filterQuery.LatitudeGreaterOrEqualThan, - // LatitudeLessOrEqualThan = - // filterQuery.LatitudeLessOrEqualThan, - // VehicleType = VehicleType.FromName(filterQuery.VehicleType), - // CountryGuid = filterQuery.CountryUuid, - // RegionGuid = filterQuery.RegionUuid, - // CityGuid = filterQuery.CityUuid - // }, - // cancellationToken); - // } - // - // [HttpGet("{uuid:guid}")] - // [SwaggerOperation("Get a ticketGroup by uuid")] - // [SwaggerResponse( - // StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", typeof(TicketGroupDto))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Get( - // [FromRoute] Guid uuid, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send(new GetTicketGroupQuery() { Guid = uuid }, - // cancellationToken); - // } - // - // [HttpPut("{uuid:guid}")] - // [SwaggerOperation("Update a ticketGroup")] - // [SwaggerResponse( - // StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Object already exists", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", typeof(TicketGroupDto))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Parent object not found", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Update( - // [FromRoute] Guid uuid, - // [FromBody] UpdateTicketGroupViewModel viewModel, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send( - // new UpdateTicketGroupCommand() - // { - // Guid = uuid, - // Name = viewModel.Name, - // Longitude = viewModel.Longitude, - // Latitude = viewModel.Latitude, - // VehicleType = VehicleType.FromName(viewModel.VehicleType), - // CityGuid = viewModel.CityUuid - // }, - // cancellationToken); - // } - // - // [HttpDelete("{uuid:guid}")] - // [SwaggerOperation("Delete a ticketGroup")] - // [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] - // [SwaggerResponse( - // StatusCodes.Status400BadRequest, "Input data validation error", - // typeof(HttpValidationProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status403Forbidden, - // "Not enough privileges to perform an action", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status404NotFound, "Object not found", - // typeof(ProblemDetails))] - // [SwaggerResponse( - // StatusCodes.Status500InternalServerError, "Internal server error", - // typeof(ProblemDetails))] - // public async Task Delete( - // [FromRoute] Guid uuid, - // CancellationToken cancellationToken) - // { - // await Mediator.Send( - // new DeleteTicketGroupCommand() { Guid = uuid }, - // cancellationToken); - // return StatusCode(StatusCodes.Status204NoContent); - // } + [HttpGet] + [SwaggerOperation("Get a list of all ticket groups")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetTicketGroupsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetTicketGroupsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + PassangerSex = filterQuery.PassangerSex? + .Select(s => Sex.FromName(s)).ToHashSet(), + PassangerBirthDateGreaterThanOrEqualTo = + filterQuery.PassangerBirthDateGreaterThanOrEqualTo, + PassangerBirthDateLessThanOrEqualTo = + filterQuery.PassangerBirthDateLessThanOrEqualTo, + PurchaseTimeGreaterThanOrEqualTo = + filterQuery.PurchaseTimeGreaterThanOrEqualTo, + PurchaseTimeLessThanOrEqualTo = + filterQuery.PurchaseTimeLessThanOrEqualTo, + Statuses = filterQuery.Statuses? + .Select(s => TicketStatus.FromName(s)).ToHashSet(), + VehicleTypes = filterQuery.VehicleTypes? + .Select(vt => VehicleType.FromName(vt)).ToHashSet(), + TravelTimeGreaterThanOrEqualTo = + filterQuery.TravelTimeGreaterThanOrEqualTo, + TravelTimeLessThanOrEqualTo = + filterQuery.TravelTimeLessThanOrEqualTo + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a ticket group by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetTicketGroupQuery() { Guid = uuid }, + cancellationToken); + } } From 120963f3cc57c9ef795e2619dca0956212be09ac Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 30 May 2025 16:40:28 +0300 Subject: [PATCH 34/35] add optional ticket group binding to account --- .../GetPaymentLinkCommandAuthorizer.cs | 20 +- .../GetPaymentLinkCommandHandler.cs | 13 +- .../AddTicketGroupCommandHandler.cs | 3 +- .../GetTicketGroupQueryHandler.cs | 8 + .../GetTicketGroupsPageQuery.cs | 2 + .../GetTicketGroupsPageQueryHandler.cs | 23 +- .../TicketGroups/TicketGroupAccountDto.cs | 21 + .../TicketGroups/TicketGroupDto.cs | 1 + .../GetTicketGroupsPageFilterViewModel.cs | 2 + src/Domain/Entities/Account.cs | 2 + src/Domain/Entities/TicketGroup.cs | 5 + .../Controllers/TicketGroupsController.cs | 3 +- .../Configurations/CompanyConfiguration.cs | 2 +- .../Configurations/EmployeeConfiguration.cs | 2 +- .../TicketGroupConfiguration.cs | 24 + ...n_from_Ticket_Group_to_Account.Designer.cs | 1359 +++++++++++++++++ ...navigation_from_Ticket_Group_to_Account.cs | 114 ++ .../PostgreSqlDbContextModelSnapshot.cs | 24 +- 18 files changed, 1597 insertions(+), 31 deletions(-) create mode 100644 src/Application/TicketGroups/TicketGroupAccountDto.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs index 15e36c4..b1c8fb0 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs @@ -1,6 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Payments.LiqPay @@ -9,24 +7,8 @@ namespace cuqmbr.TravelGuide.Application.Payments.LiqPay public class GetPaymentLinkCommandAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public GetPaymentLinkCommandAuthorizer(SessionUserService sessionUserService) - { - _sessionUserService = sessionUserService; - } - public override void BuildPolicy(GetPaymentLinkCommand request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated= _sessionUserService.IsAuthenticated - }); - - UseRequirement(new MustBeInRolesRequirement - { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs index dcdd08f..123c931 100644 --- a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -25,6 +25,7 @@ public class GetPaymentLinkCommandHandler : private readonly SessionTimeZoneService _sessionTimeZoneService; private readonly SessionCultureService _sessionCultureService; + private readonly SessionUserService _sessionUserService; public GetPaymentLinkCommandHandler( UnitOfWork unitOfWork, @@ -33,7 +34,8 @@ public class GetPaymentLinkCommandHandler : IStringLocalizer localizer, EmailSenderService emailSender, SessionTimeZoneService SessionTimeZoneService, - SessionCultureService sessionCultureService) + SessionCultureService sessionCultureService, + SessionUserService sessionUserService) { _unitOfWork = unitOfWork; _currencyConverterService = currencyConverterService; @@ -42,6 +44,7 @@ public class GetPaymentLinkCommandHandler : _emailSender = emailSender; _sessionTimeZoneService = SessionTimeZoneService; _sessionCultureService = sessionCultureService; + _sessionUserService = sessionUserService; } public async Task Handle( @@ -409,6 +412,10 @@ public class GetPaymentLinkCommandHandler : .Items; + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == _sessionUserService.Guid, cancellationToken); + + var travelTime = ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - ticketsDetails.OrderBy(td => td.order).First().departureTime; @@ -451,8 +458,8 @@ public class GetPaymentLinkCommandHandler : Currency = detail.currency, VehicleEnrollmentId = ve.Id }; - }) - .ToArray() + }).ToArray(), + AccountId = account?.Id }; entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs index e6f0c3a..9854582 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -148,7 +148,7 @@ public class AddTicketGroupCommandHandler : var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository .GetPageAsync( e => vehicleEnrollmentGuids.Contains(e.Guid), - e => e.Vehicle, + e => e.Vehicle.Company, 1, vehicleEnrollmentGuids.Count(), cancellationToken)) .Items; @@ -416,7 +416,6 @@ public class AddTicketGroupCommandHandler : 1, vehicleEnrollmentGuids.Count(), cancellationToken)) .Items; - var routeAddressGuids = request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs index f3c270c..e8e50e0 100644 --- a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs @@ -85,6 +85,14 @@ public class GetTicketGroupQueryHandler : 1, vehicleIds.Count(), cancellationToken)) .Items; + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == ticketGroup.AccountId, cancellationToken); + + if (ticketGroup.AccountId != null) + { + ticketGroup.Account = account; + } + foreach (var ve in vehicleEnrollments) { ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs index 2d0a9f9..4f85a19 100644 --- a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs @@ -32,6 +32,8 @@ public record GetTicketGroupsPageQuery : IRequest> public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + public Guid? AccountGuid { get; set; } + // TODO: Add filtering parametetrs listed below. It is hard to // be done because of pagination. diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs index a7c0e2b..e350e7d 100644 --- a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs @@ -76,6 +76,9 @@ public class GetTicketGroupsPageQueryHandler : : true) && (request.TravelTimeLessThanOrEqualTo != null ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true) && + (request.AccountGuid != null + ? e.Account.Guid == request.AccountGuid : true), e => e.Tickets, request.PageNumber, request.PageSize, cancellationToken); @@ -123,6 +126,22 @@ public class GetTicketGroupsPageQueryHandler : 1, vehicleIds.Count(), cancellationToken)) .Items; + var accountIds = + ticketGroups.Select(tg => tg.AccountId); + var accounts = (await _unitOfWork.AccountRepository + .GetPageAsync( + a => accountIds.Contains(a.Id), + 1, accountIds.Count(), cancellationToken)) + .Items; + + foreach (var tg in ticketGroups) + { + if (tg.AccountId != null) + { + tg.Account = accounts.Single(a => a.Id == tg.AccountId); + } + } + foreach (var ve in vehicleEnrollments) { ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); @@ -137,8 +156,8 @@ public class GetTicketGroupsPageQueryHandler : } - // TODO: Replace with AutoMapper resolvers - // Convert currency and apply session time zone + // TODO: Replace with AutoMapper resolvers. + // Convert currency and apply session time zone. var convertTasks = new List(); var processedRouteAddressDetailIds = new HashSet(); diff --git a/src/Application/TicketGroups/TicketGroupAccountDto.cs b/src/Application/TicketGroups/TicketGroupAccountDto.cs new file mode 100644 index 0000000..c439d72 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupAccountDto.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupAccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupDto.cs b/src/Application/TicketGroups/TicketGroupDto.cs index 6c5b3c3..bdbbae0 100644 --- a/src/Application/TicketGroups/TicketGroupDto.cs +++ b/src/Application/TicketGroups/TicketGroupDto.cs @@ -39,6 +39,7 @@ public sealed class TicketGroupDto : IMapFrom public decimal Cost { get; set; } + public TicketGroupAccountDto? Account { get; set; } public ICollection Enrollments { get; set; } diff --git a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs index cee2f12..f60135d 100644 --- a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs +++ b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs @@ -19,4 +19,6 @@ public sealed class GetTicketGroupsPageFilterViewModel public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public Guid? AccountUuid { get; set; } } diff --git a/src/Domain/Entities/Account.cs b/src/Domain/Entities/Account.cs index b3b0464..983203d 100644 --- a/src/Domain/Entities/Account.cs +++ b/src/Domain/Entities/Account.cs @@ -18,4 +18,6 @@ public sealed class Account : EntityBase public Employee? Employee { get; set; } public Company? Company { get; set; } + + public ICollection TicketGroups { get; set; } } diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs index b532b78..66ac0ba 100644 --- a/src/Domain/Entities/TicketGroup.cs +++ b/src/Domain/Entities/TicketGroup.cs @@ -24,4 +24,9 @@ public sealed class TicketGroup : EntityBase public ICollection Tickets { get; set; } + + + public long? AccountId { get; set; } + + public Account? Account { get; set; } } diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs index fff0d39..a1291d7 100644 --- a/src/HttpApi/Controllers/TicketGroupsController.cs +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -119,7 +119,8 @@ public class TicketGroupsController : ControllerBase TravelTimeGreaterThanOrEqualTo = filterQuery.TravelTimeGreaterThanOrEqualTo, TravelTimeLessThanOrEqualTo = - filterQuery.TravelTimeLessThanOrEqualTo + filterQuery.TravelTimeLessThanOrEqualTo, + AccountGuid = filterQuery.AccountUuid }, cancellationToken); } diff --git a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs index bc1b29b..0f0a923 100644 --- a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs @@ -54,7 +54,7 @@ public class CompanyConfiguration : BaseConfiguration "fk_" + $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.ClientNoAction); builder .HasIndex(c => c.AccountId) diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs index 3aae0e9..345af64 100644 --- a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs @@ -69,7 +69,7 @@ public class EmployeeConfiguration : BaseConfiguration "fk_" + $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.ClientNoAction); builder .HasIndex(e => e.CompanyId) diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs index 017bbd4..3c29245 100644 --- a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -89,5 +89,29 @@ public class TicketGroupConfiguration : BaseConfiguration .HasColumnName("travel_time") .HasColumnType("interval") .IsRequired(true); + + + builder + .Property(tg => tg.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(false); + + builder + .HasOne(tg => tg.Account) + .WithMany(a => a.TicketGroups) + .HasForeignKey(tg => tg.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.SetNull); + + builder + .HasIndex(c => c.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs new file mode 100644 index 0000000..b5ee921 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs @@ -0,0 +1,1359 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250530101737_Add_navigation_from_Ticket_Group_to_Account")] + partial class Add_navigation_from_Ticket_Group_to_Account + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_ticket_groups_account_id"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("TicketGroups") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_ticket_groups_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + + b.Navigation("TicketGroups"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs new file mode 100644 index 0000000..8959a4b --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Ticket_Group_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees"); + + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "ticket_groups", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_ticket_groups_account_id", + schema: "application", + table: "ticket_groups", + column: "account_id"); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_ticket_groups_account_id", + schema: "application", + table: "ticket_groups", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropForeignKey( + name: "fk_ticket_groups_account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropIndex( + name: "ix_ticket_groups_account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 99901fb..9587b06 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -729,6 +729,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + b.Property("Guid") .HasColumnType("uuid") .HasColumnName("uuid"); @@ -780,6 +784,9 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_ticket_groups_uuid"); + b.HasIndex("AccountId") + .HasDatabaseName("ix_ticket_groups_account_id"); + b.ToTable("ticket_groups", "application", t => { t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); @@ -1053,7 +1060,7 @@ namespace Persistence.PostgreSql.Migrations b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") .WithOne("Company") .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.ClientNoAction) .IsRequired() .HasConstraintName("fk_companies_account_id"); @@ -1072,7 +1079,7 @@ namespace Persistence.PostgreSql.Migrations b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") .WithMany("Employees") .HasForeignKey("CompanyId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.ClientNoAction) .IsRequired() .HasConstraintName("fk_employees_company_id"); @@ -1196,6 +1203,17 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("VehicleEnrollment"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("TicketGroups") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_ticket_groups_account_id"); + + b.Navigation("Account"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") @@ -1259,6 +1277,8 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Employee"); b.Navigation("RefreshTokens"); + + b.Navigation("TicketGroups"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => From 0508c89c2dcab17ae6801c03b83ff64374b1a05f Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 3 Jun 2025 18:00:07 +0300 Subject: [PATCH 35/35] add authorization requirements --- .../AddAddress/AddAddressCommandAuthorizer.cs | 5 ++- .../DeleteAddressCommandAuthorizer.cs | 2 +- .../UpdateAddressCommandAuthorizer.cs | 2 +- .../GetAddress/GetAddressQueryAuthorizer.cs | 7 ++-- .../GetAddressesPageQueryAuthorizer.cs | 7 ++-- .../AddAircraftCommandAuthorizer.cs | 23 +++++++--- .../DeleteAircraftCommandAuthorizer.cs | 23 +++++++--- .../UpdateAircraftCommandAuthorizer.cs | 23 +++++++--- .../GetAircraft/GetAircraftQueryAuthorizer.cs | 23 +++++++--- .../GetAircraftsPageQueryAuthorizer.cs | 23 +++++++--- .../RenewAccessTokenCommandAuthorizer.cs | 12 ------ .../RevokeRefreshTokenCommandAuthorizer.cs | 13 +++++- .../AddBus/AddBusCommandAuthorizer.cs | 23 +++++++--- .../DeleteBus/DeleteBusCommandAuthorizer.cs | 23 +++++++--- .../UpdateBus/UpdateBusCommandAuthorizer.cs | 23 +++++++--- .../Queries/GetBus/GetBusQueryAuthorizer.cs | 23 +++++++--- .../GetBusesPageQueryAuthorizer.cs | 23 +++++++--- .../AddCity/AddCityCommandAuthorizer.cs | 2 +- .../DeleteCity/DeleteCityCommandAuthorizer.cs | 2 +- .../UpdateCity/UpdateCityCommandAuthorizer.cs | 2 +- .../GetCitiesPageQueryAuthorizer.cs | 7 ++-- .../Queries/GetCity/GetCityQueryAuthorizer.cs | 7 ++-- ...nt.cs => MustBeInAnyOfRolesRequirement.cs} | 21 +++------- .../MustBeObjectOwnerOrAdminRequirement.cs | 42 +++++++++++++++++++ .../AddCompany/AddCompanyCommandAuthorizer.cs | 4 +- .../DeleteCompanyCommandAuthorizer.cs | 4 +- .../UpdateCompanyCommandAuthorizer.cs | 23 +++++++--- .../GetCompaniesPageQueryAuthorizer.cs | 20 +-------- .../GetCompany/GetCompanyQueryAuthorizer.cs | 20 +-------- .../AddCountry/AddCountryCommandAuthorizer.cs | 2 +- .../DeleteCountryCommandAuthorizer.cs | 2 +- .../UpdateCountryCommandAuthorizer.cs | 2 +- .../GetCountriesPageQueryAuthorizer.cs | 7 ++-- .../GetCountry/GetCountryQueryAuthorizer.cs | 7 ++-- .../AddEmployeeCommandAuthorizer.cs | 23 +++++++--- .../DeleteEmployeeCommandAuthorizer.cs | 23 +++++++--- .../UpdateEmployeeCommandAuthorizer.cs | 23 +++++++--- .../GetEmployee/GetEmployeeQueryAuthorizer.cs | 23 +++++++--- .../GetEmployeesPageQueryAuthorizer.cs | 23 +++++++--- .../AddAccount/AddAccountCommandAuthorizer.cs | 2 +- .../DeleteAccountCommandAuthorizer.cs | 2 +- .../UpdateAccountCommandAuthorizer.cs | 2 +- .../GetAccount/GetAccountQueryAuthorizer.cs | 2 +- .../GetAccountsPageQueryAuthorizer.cs | 2 +- .../GetRolesPageQueryAuthorizer.cs | 2 +- .../AddRegion/AddRegionCommandAuthorizer.cs | 2 +- .../DeleteRegionCommandAuthorizer.cs | 2 +- .../UpdateRegionCommandAuthorizer.cs | 2 +- .../GetRegion/GetRegionQueryAuthorizer.cs | 5 ++- .../GetRegionsPageQueryAuthorizer.cs | 5 ++- .../AddRoute/AddRouteCommandAuthorizer.cs | 7 ++-- .../DeleteRouteCommandAuthorizer.cs | 2 +- .../UpdateRouteCommandAuthorizer.cs | 2 +- .../GetRoute/GetRouteQueryAuthorizer.cs | 7 ++-- .../GetRoutesPageQueryAuthorizer.cs | 5 ++- .../AddTicketGroupCommandAuthorizer.cs | 4 +- .../GetTicketGroupQueryAuthorizer.cs | 23 +++++++--- .../GetTicketGroupsPageQueryAuthorizer.cs | 22 +++++++--- .../AddTrain/AddTrainCommandAuthorizer.cs | 23 +++++++--- .../DeleteTrainCommandAuthorizer.cs | 23 +++++++--- .../UpdateTrainCommandAuthorizer.cs | 23 +++++++--- .../GetTrain/GetTrainQueryAuthorizer.cs | 23 +++++++--- .../GetTrainsPageQueryAuthorizer.cs | 23 +++++++--- .../SearchAll/SearchAllQueryAuthorizer.cs | 21 +--------- .../SearchShortestQueryAuthorizer.cs | 21 +--------- .../AddVehicleEnrollmentCommandAuthorizer.cs | 40 +++++++++++++++--- ...eleteVehicleEnrollmentCommandAuthorizer.cs | 23 +++++++--- ...pdateVehicleEnrollmentCommandAuthorizer.cs | 39 ++++++++++++++--- .../GetVehicleEnrollmentQueryAuthorizer.cs | 23 +++++++--- ...etVehicleEnrollmentsPageQueryAuthorizer.cs | 23 +++++++--- 70 files changed, 641 insertions(+), 331 deletions(-) rename src/Application/Common/Authorization/{MustBeInRolesRequirement.cs => MustBeInAnyOfRolesRequirement.cs} (57%) create mode 100644 src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs index 6c88c27..b4bbe78 100644 --- a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs @@ -22,9 +22,10 @@ public class AddAddressCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs index 7b5bb31..66a9511 100644 --- a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class DeleteAddressCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs index 80860d1..055fd69 100644 --- a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateAddressCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs index d6cfa6c..9bb9784 100644 --- a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetAddressQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs index 6521dde..c868ec9 100644 --- a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetAddressesPageQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs index 46621fb..69c650f 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; @@ -9,23 +9,34 @@ public class AddAircraftCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public AddAircraftCommandAuthorizer(SessionUserService sessionUserService) + public AddAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(AddAircraftCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs index 1aeae2a..295c08b 100644 --- a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; @@ -9,23 +9,34 @@ public class DeleteAircraftCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public DeleteAircraftCommandAuthorizer(SessionUserService sessionUserService) + public DeleteAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(DeleteAircraftCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs index b15c281..a23a8bf 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; @@ -9,23 +9,34 @@ public class UpdateAircraftCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public UpdateAircraftCommandAuthorizer(SessionUserService sessionUserService) + public UpdateAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateAircraftCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs index a0cc4e2..aac2bd3 100644 --- a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; @@ -9,23 +9,34 @@ public class GetAircraftQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetAircraftQueryAuthorizer(SessionUserService sessionUserService) + public GetAircraftQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetAircraftQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs index 41b94bb..2871ed3 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; @@ -9,23 +9,34 @@ public class GetAircraftsPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetAircraftsPageQueryAuthorizer(SessionUserService sessionUserService) + public GetAircraftsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetAircraftsPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs index 3d102fc..96f0a24 100644 --- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs @@ -1,5 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -// using cuqmbr.TravelGuide.Application.Common.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; @@ -7,19 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken public class RenewAccessTokenCommandAuthorizer : AbstractRequestAuthorizer { - // private readonly SessionUserService _sessionUserService; - // - // public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService) - // { - // _sessionUserService = currentUserService; - // } - public override void BuildPolicy(RenewAccessTokenCommand request) { UseRequirement(new AllowAllRequirement()); - // UseRequirement(new MustBeAuthenticatedRequirement - // { - // IsAuthenticated = _sessionUserService.IsAuthenticated - // }); } } diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs index c0fe1d9..6ee840d 100644 --- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; @@ -6,8 +7,18 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok public class RevokeRefreshTokenCommandAuthorizer : AbstractRequestAuthorizer { + private readonly SessionUserService _sessionUserService; + + public RevokeRefreshTokenCommandAuthorizer(SessionUserService currentUserService) + { + _sessionUserService = currentUserService; + } + public override void BuildPolicy(RevokeRefreshTokenCommand request) { - UseRequirement(new AllowAllRequirement()); + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs index 5e16639..ad47b23 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; @@ -9,23 +9,34 @@ public class AddBusCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public AddBusCommandAuthorizer(SessionUserService sessionUserService) + public AddBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(AddBusCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs index f5858c7..24e4d72 100644 --- a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; @@ -9,23 +9,34 @@ public class DeleteBusCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public DeleteBusCommandAuthorizer(SessionUserService sessionUserService) + public DeleteBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(DeleteBusCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs index 1b05021..11be53a 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; @@ -9,23 +9,34 @@ public class UpdateBusCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public UpdateBusCommandAuthorizer(SessionUserService sessionUserService) + public UpdateBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateBusCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs index a22724c..1a898b1 100644 --- a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; @@ -9,23 +9,34 @@ public class GetBusQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetBusQueryAuthorizer(SessionUserService sessionUserService) + public GetBusQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetBusQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs index 8c26e31..74b0c99 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; @@ -9,23 +9,34 @@ public class GetBusesPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetBusesPageQueryAuthorizer(SessionUserService sessionUserService) + public GetBusesPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetBusesPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs index 18d5425..32ff5c8 100644 --- a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class AddCityCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs index f966a4e..9295ca7 100644 --- a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class DeleteCityCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs index 263feaa..376f57d 100644 --- a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateCityCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs index e52cd0d..a14f95d 100644 --- a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetCitiesPageQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs index 875847f..d811478 100644 --- a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetCityQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs similarity index 57% rename from src/Application/Common/Authorization/MustBeInRolesRequirement.cs rename to src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs index 8035dd0..4a293fb 100644 --- a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs +++ b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs @@ -1,31 +1,22 @@ using MediatR.Behaviors.Authorization; -using Microsoft.Extensions.Localization; using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.Common.Authorization; -public class MustBeInRolesRequirement : IAuthorizationRequirement +public class MustBeInAnyOfRolesRequirement : IAuthorizationRequirement { public ICollection UserRoles { get; init; } public ICollection RequiredRoles { get; init; } - class MustBeInRolesRequirementHandler : - IAuthorizationHandler + class MustBeInAnyOfRolesRequirementHandler : + IAuthorizationHandler { - private readonly IStringLocalizer _localizer; - - public MustBeInRolesRequirementHandler(IStringLocalizer localizer) - { - _localizer = localizer; - } - public Task Handle( - MustBeInRolesRequirement request, + MustBeInAnyOfRolesRequirement request, CancellationToken cancellationToken) { - var isUserInRequiredRoles = - request.UserRoles?.Any(ur => request.RequiredRoles.Contains(ur)) - ?? false; + var isUserInRequiredRoles = request.UserRoles + .Any(ur => request.RequiredRoles.Contains(ur)); if (!isUserInRequiredRoles) { diff --git a/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs new file mode 100644 index 0000000..d33ffc3 --- /dev/null +++ b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs @@ -0,0 +1,42 @@ +using MediatR.Behaviors.Authorization; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class MustBeObjectOwnerOrAdminRequirement : IAuthorizationRequirement +{ + public ICollection? UserRoles { get; init; } + + public Guid? UserGuid { get; init; } + public Guid? RequiredGuid { get; init; } + + class MustBeObjectOwnerOrAdminRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + MustBeObjectOwnerOrAdminRequirement request, + CancellationToken cancellationToken) + { + var isAdmin = request?.UserRoles + ?.Any(ur => ur.Equals(IdentityRole.Administrator)) ?? + false; + + if (isAdmin) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + + if (request?.UserGuid == null || request?.RequiredGuid == null) + { + return Task.FromResult(AuthorizationResult.Fail()); + } + + if (request.UserGuid == request.RequiredGuid) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + + return Task.FromResult(AuthorizationResult.Fail()); + } + } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs index ea238b8..d7f9f45 100644 --- a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs @@ -19,10 +19,10 @@ public class AddCompanyCommandAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs index 446421d..d71fa4e 100644 --- a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs @@ -19,10 +19,10 @@ public class DeleteCompanyCommandAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs index a3f754b..b1b41da 100644 --- a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; @@ -9,23 +9,34 @@ public class UpdateCompanyCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public UpdateCompanyCommandAuthorizer(SessionUserService sessionUserService) + public UpdateCompanyCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateCompanyCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs index 3ed1e1a..d449f07 100644 --- a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs @@ -1,6 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; @@ -8,24 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; public class GetCompaniesPageQueryAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public GetCompaniesPageQueryAuthorizer(SessionUserService sessionUserService) - { - _sessionUserService = sessionUserService; - } - public override void BuildPolicy(GetCompaniesPageQuery request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated= _sessionUserService.IsAuthenticated - }); - - UseRequirement(new MustBeInRolesRequirement - { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs index f83a0b8..eadbd4d 100644 --- a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs @@ -1,6 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; @@ -8,24 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; public class GetCompanyQueryAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public GetCompanyQueryAuthorizer(SessionUserService sessionUserService) - { - _sessionUserService = sessionUserService; - } - public override void BuildPolicy(GetCompanyQuery request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated= _sessionUserService.IsAuthenticated - }); - - UseRequirement(new MustBeInRolesRequirement - { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs index 930de98..832728e 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class AddCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs index 1c327b1..8492015 100644 --- a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class DeleteCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs index dcdfbe7..1279663 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs index 24ac4df..5b527aa 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetCountriesPageQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs index f158344..a57b9d9 100644 --- a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetCountryQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs index 1255c27..cb39878 100644 --- a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; @@ -9,23 +9,34 @@ public class AddEmployeeCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public AddEmployeeCommandAuthorizer(SessionUserService sessionUserService) + public AddEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(AddEmployeeCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs index f5b826f..9d07ec0 100644 --- a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; @@ -9,23 +9,34 @@ public class DeleteEmployeeCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public DeleteEmployeeCommandAuthorizer(SessionUserService sessionUserService) + public DeleteEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(DeleteEmployeeCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var employee = _unitOfWork.EmployeeRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs index 52bd256..dcc01e3 100644 --- a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; @@ -9,23 +9,34 @@ public class UpdateEmployeeCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public UpdateEmployeeCommandAuthorizer(SessionUserService sessionUserService) + public UpdateEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateEmployeeCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs index 5334ff2..00535f8 100644 --- a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; @@ -9,23 +9,34 @@ public class GetEmployeeQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetEmployeeQueryAuthorizer(SessionUserService sessionUserService) + public GetEmployeeQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetEmployeeQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var employee = _unitOfWork.EmployeeRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs index b86a3cb..12b2cae 100644 --- a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; @@ -9,23 +9,34 @@ public class GetEmployeesPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetEmployeesPageQueryAuthorizer(SessionUserService sessionUserService) + public GetEmployeesPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetEmployeesPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs index 2e62d6b..5cf1ed3 100644 --- a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs @@ -23,7 +23,7 @@ public class AddAccountCommandAuthorizer : IsAuthenticated= _sessionAccountService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionAccountService.Roles diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs index 4019940..8898f22 100644 --- a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs @@ -23,7 +23,7 @@ public class DeleteAccountCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs index ed54a5a..023bceb 100644 --- a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateAccountCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs index 56954d3..99ec778 100644 --- a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs @@ -22,7 +22,7 @@ public class GetAccountQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs index 79d158a..53a7a30 100644 --- a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs @@ -22,7 +22,7 @@ public class GetAccountsPageQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs index cc167f7..c21d365 100644 --- a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs @@ -22,7 +22,7 @@ public class GetRolesPageQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs index 7accab4..f2b0ec6 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class AddRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs index 07f9266..3d5a52c 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class DeleteRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs index 2ef62f7..7f7d408 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs index f3fd0f3..2d56d94 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs @@ -22,9 +22,10 @@ public class GetRegionQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs index 248159e..1f9fb10 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs @@ -22,9 +22,10 @@ public class GetRegionsPageQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs index 559ff04..5240e74 100644 --- a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs @@ -19,12 +19,13 @@ public class AddRouteCommandAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs index ec3a774..fd47ef4 100644 --- a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class DeleteRouteCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs index b4f32e6..1f714da 100644 --- a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs @@ -22,7 +22,7 @@ public class UpdateRouteCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs index 16ec495..eba39e0 100644 --- a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs @@ -19,12 +19,13 @@ public class GetRouteQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs index 57e0d8e..62ae9a3 100644 --- a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs @@ -22,9 +22,10 @@ public class GetRoutesPageQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs index 256e9a3..192bf72 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs @@ -19,10 +19,10 @@ public class AddTicketGroupCommandAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs index 4305de1..b9b0782 100644 --- a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; @@ -9,23 +9,34 @@ public class GetTicketGroupQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetTicketGroupQueryAuthorizer(SessionUserService sessionUserService) + public GetTicketGroupQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetTicketGroupQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var ticketGroup = _unitOfWork.TicketGroupRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Account!, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = ticketGroup?.Account?.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs index ba67439..0a105d6 100644 --- a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; @@ -9,23 +9,33 @@ public class GetTicketGroupsPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetTicketGroupsPageQueryAuthorizer(SessionUserService sessionUserService) + public GetTicketGroupsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetTicketGroupsPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var account = _unitOfWork.AccountRepository + .GetOneAsync( + e => e.Guid == request.AccountGuid, CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = account?.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs index 256c7ea..ad27294 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; @@ -9,23 +9,34 @@ public class AddTrainCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public AddTrainCommandAuthorizer(SessionUserService sessionUserService) + public AddTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(AddTrainCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs index 9e888b6..8b3be6d 100644 --- a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; @@ -9,23 +9,34 @@ public class DeleteTrainCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public DeleteTrainCommandAuthorizer(SessionUserService sessionUserService) + public DeleteTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(DeleteTrainCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs index 6a5f59c..01985f6 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; @@ -9,23 +9,34 @@ public class UpdateTrainCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public UpdateTrainCommandAuthorizer(SessionUserService sessionUserService) + public UpdateTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateTrainCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs index dc417e7..e471e0d 100644 --- a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; @@ -9,23 +9,34 @@ public class GetTrainQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetTrainQueryAuthorizer(SessionUserService sessionUserService) + public GetTrainQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetTrainQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs index 5ce63f8..a8acb71 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; @@ -9,23 +9,34 @@ public class GetTrainsPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetTrainsPageQueryAuthorizer(SessionUserService sessionUserService) + public GetTrainsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetTrainsPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs index fbc4307..4032b4b 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs @@ -1,6 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application @@ -9,25 +7,8 @@ namespace cuqmbr.TravelGuide.Application public class SearchAllQueryAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public SearchAllQueryAuthorizer( - SessionUserService sessionUserService) - { - _sessionUserService = sessionUserService; - } - public override void BuildPolicy(SearchAllQuery request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated= _sessionUserService.IsAuthenticated - }); - - UseRequirement(new MustBeInRolesRequirement - { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs index 055a5d4..0141b59 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs @@ -1,6 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application @@ -9,25 +7,8 @@ namespace cuqmbr.TravelGuide.Application public class SearchShortestQueryAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public SearchShortestQueryAuthorizer( - SessionUserService sessionUserService) - { - _sessionUserService = sessionUserService; - } - public override void BuildPolicy(SearchShortestQuery request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated= _sessionUserService.IsAuthenticated - }); - - UseRequirement(new MustBeInRolesRequirement - { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs index 38f64d6..cf7e8db 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments @@ -10,23 +10,51 @@ public class AddVehicleEnrollmentCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public AddVehicleEnrollmentCommandAuthorizer(SessionUserService sessionUserService) + public AddVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(AddVehicleEnrollmentCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicle = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.VehicleGuid, e => e.Company.Account, + CancellationToken.None) + .Result; + + var employees = _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + e => e.Company.Account, + 1, request.EmployeeGuids.Count, CancellationToken.None) + .Result.Items; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicle?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); + + foreach (var employee in employees) + { + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } } } diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs index 4f9c61f..d880e73 100644 --- a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; @@ -9,23 +9,34 @@ public class DeleteVehicleEnrollmentCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public DeleteVehicleEnrollmentCommandAuthorizer(SessionUserService sessionUserService) + public DeleteVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(DeleteVehicleEnrollmentCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicelEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicelEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs index ad1f9d1..ddc7bef 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments @@ -10,24 +10,51 @@ public class UpdateVehicleEnrollmentCommandAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; public UpdateVehicleEnrollmentCommandAuthorizer( - SessionUserService sessionUserService) + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(UpdateVehicleEnrollmentCommand request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicleEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + var employees = _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + e => e.Company.Account, + 1, request.EmployeeGuids.Count, CancellationToken.None) + .Result.Items; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicleEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); + + foreach (var employee in employees) + { + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } } } diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs index a026fff..32e3d60 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments @@ -10,23 +10,34 @@ public class GetVehicleEnrollmentQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetVehicleEnrollmentQueryAuthorizer(SessionUserService sessionUserService) + public GetVehicleEnrollmentQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetVehicleEnrollmentQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicelEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicelEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } } diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs index 4d69da0..ec0cbaa 100644 --- a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; @@ -9,23 +9,34 @@ public class GetVehicleEnrollmentsPageQueryAuthorizer : AbstractRequestAuthorizer { private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; - public GetVehicleEnrollmentsPageQueryAuthorizer(SessionUserService sessionUserService) + public GetVehicleEnrollmentsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) { _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; } public override void BuildPolicy(GetVehicleEnrollmentsPageQuery request) { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + var vehicles = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.VehicleGuid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement { - RequiredRoles = [IdentityRole.Administrator], - UserRoles = _sessionUserService.Roles + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicles?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid }); } }