diff --git a/.gitignore b/.gitignore index 154e127..eba539d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # User-specific files *.rsuser + *.suo *.user *.userosscache @@ -13,8 +14,6 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -# Mono auto generated files -mono_crash.* # Build results [Dd]ebug/ @@ -23,14 +22,12 @@ mono_crash.* [Rr]eleases/ x64/ x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ + bld/ [Bb]in/ [Oo]bj/ [Ll]og/ -[Ll]ogs/ + # Visual Studio 2015/2017 cache/options directory .vs/ @@ -44,10 +41,7 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml + # Build Results of an ATL Project [Dd]ebugPS/ @@ -68,13 +62,13 @@ artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt + # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c -*_h.h *.ilk *.meta *.obj @@ -197,8 +191,6 @@ PublishScripts/ # NuGet Packages *.nupkg -# NuGet Symbol Packages -*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -223,14 +215,13 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx -*.appxbundle -*.appxupload + # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!?*.[Cc]ache/ + # Others ClientBin/ @@ -239,11 +230,13 @@ ClientBin/ *.dbmdl *.dbproj.schemaview *.jfm + *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk + # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -274,9 +267,7 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl + # Microsoft Fakes FakesAssemblies/ @@ -323,8 +314,13 @@ paket-files/ # FAKE - F# Make .fake/ -# CodeRush personal settings -.cr/personal + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ @@ -349,7 +345,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output + ASALocalRun/ # MSBuild Binary and Structured Log @@ -475,3 +471,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + diff --git a/Brewery.sln b/Brewery.sln new file mode 100644 index 0000000..ccd6546 --- /dev/null +++ b/Brewery.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{13BABC4E-2013-45BE-B5F2-37374813BFF3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FD1F28D7-23D0-4225-814C-549E308E4C9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Api", "src\Brewery.Api\Brewery.Api.csproj", "{CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Domain", "src\Brewery.Domain\Brewery.Domain.csproj", "{89613C40-A24A-4972-909F-8E840056349A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Application", "src\Brewery.Application\Brewery.Application.csproj", "{6DA2F545-AD92-4EF3-8638-D42F1F8C5833}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Infrastructure", "src\Brewery.Infrastructure\Brewery.Infrastructure.csproj", "{F0FFE48E-371C-423C-9DB7-22BF810F6AA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Abstractions", "src\Brewery.Abstractions\Brewery.Abstractions.csproj", "{AD694077-201B-49C3-9537-FE96BA598E82}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Tests.Shared", "tests\Brewery.Tests.Shared\Brewery.Tests.Shared.csproj", "{8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Tests.EndToEnd", "tests\Brewery.Tests.EndToEnd\Brewery.Tests.EndToEnd.csproj", "{667A8402-FC19-406C-9BCA-4D194D1AD257}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {89613C40-A24A-4972-909F-8E840056349A} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {AD694077-201B-49C3-9537-FE96BA598E82} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60} = {FD1F28D7-23D0-4225-814C-549E308E4C9D} + {667A8402-FC19-406C-9BCA-4D194D1AD257} = {FD1F28D7-23D0-4225-814C-549E308E4C9D} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Release|Any CPU.Build.0 = Release|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Release|Any CPU.Build.0 = Release|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Release|Any CPU.Build.0 = Release|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.Build.0 = Release|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.Build.0 = Release|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Release|Any CPU.Build.0 = Release|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..cee78da --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## Overview +Brewery is a RESTful API that enables breweries, wholesalers, and clients to manage and interact with a beer distribution system. The system supports functionalities for managing beer inventory, facilitating sales between brewers and wholesalers, and providing clients with quotes for bulk beer orders. The application is built using .NET Core and Entity Framework, with a focus on efficient data management and clear REST API principles. diff --git a/src/Brewery.Abstractions/Auth/IAuthManager.cs b/src/Brewery.Abstractions/Auth/IAuthManager.cs new file mode 100644 index 0000000..6c6cc09 --- /dev/null +++ b/src/Brewery.Abstractions/Auth/IAuthManager.cs @@ -0,0 +1,7 @@ +namespace Brewery.Abstractions.Auth; + +public interface IAuthManager +{ + JsonWebToken GenerateToken(string userId, string role, string audience = null, + IDictionary> claims = null); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Auth/JsonWebToken.cs b/src/Brewery.Abstractions/Auth/JsonWebToken.cs new file mode 100644 index 0000000..a3b096a --- /dev/null +++ b/src/Brewery.Abstractions/Auth/JsonWebToken.cs @@ -0,0 +1,12 @@ +namespace Brewery.Abstractions.Auth; + +public class JsonWebToken +{ + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public long Expires { get; set; } + public string Id { get; set; } + public string Role { get; set; } + public string Email { get; set; } + public IDictionary> Claims { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Brewery.Abstractions.csproj b/src/Brewery.Abstractions/Brewery.Abstractions.csproj new file mode 100644 index 0000000..49a0f4e --- /dev/null +++ b/src/Brewery.Abstractions/Brewery.Abstractions.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Brewery.Abstractions/Commands/ICommand.cs b/src/Brewery.Abstractions/Commands/ICommand.cs new file mode 100644 index 0000000..c5d0ca5 --- /dev/null +++ b/src/Brewery.Abstractions/Commands/ICommand.cs @@ -0,0 +1,10 @@ +using Brewery.Abstractions.Messaging; + +namespace Brewery.Abstractions.Commands; + +public interface ICommand : IMessage +{ + +} + +public interface ICommand : ICommand; \ No newline at end of file diff --git a/src/Brewery.Abstractions/Commands/ICommandDispatcher.cs b/src/Brewery.Abstractions/Commands/ICommandDispatcher.cs new file mode 100644 index 0000000..b391652 --- /dev/null +++ b/src/Brewery.Abstractions/Commands/ICommandDispatcher.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Commands; + +public interface ICommandDispatcher +{ + Task DispatchAsync(TCommand command) where TCommand : class, ICommand; +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Commands/ICommandHandler.cs b/src/Brewery.Abstractions/Commands/ICommandHandler.cs new file mode 100644 index 0000000..6f0b915 --- /dev/null +++ b/src/Brewery.Abstractions/Commands/ICommandHandler.cs @@ -0,0 +1,11 @@ +namespace Brewery.Abstractions.Commands; + +public interface ICommandHandler where TCommand : class, ICommand +{ + Task HandleAsync(TCommand command); +} + +public interface ICommandHandler +{ + Task HandleAsync(TCommand command); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IContext.cs b/src/Brewery.Abstractions/Contexts/IContext.cs new file mode 100644 index 0000000..b8a3147 --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IContext.cs @@ -0,0 +1,8 @@ +namespace Brewery.Abstractions.Contexts; + +public interface IContext +{ + string RequestId { get; } + string TraceId { get; } + IIdentityContext IdentityContext { get; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IContextFactory.cs b/src/Brewery.Abstractions/Contexts/IContextFactory.cs new file mode 100644 index 0000000..85d0456 --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IContextFactory.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Contexts; + +public interface IContextFactory +{ + IContext Create(); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IIdentityContext.cs b/src/Brewery.Abstractions/Contexts/IIdentityContext.cs new file mode 100644 index 0000000..e34ee6b --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IIdentityContext.cs @@ -0,0 +1,11 @@ +using System.Security.Claims; + +namespace Brewery.Abstractions.Contexts; + +public interface IIdentityContext +{ + bool IsAuthenticated { get; } + Guid Id { get; } + string Role { get; } + Dictionary> Claims { get; } +} diff --git a/src/Brewery.Abstractions/Exceptions/BreweryException.cs b/src/Brewery.Abstractions/Exceptions/BreweryException.cs new file mode 100644 index 0000000..6e4dcaf --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/BreweryException.cs @@ -0,0 +1,11 @@ +namespace Brewery.Abstractions.Exceptions; + +public class BreweryException : Exception +{ + public string Message { get; set; } + + public BreweryException(string message) + { + Message = message; + } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs b/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs new file mode 100644 index 0000000..1959c00 --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace Brewery.Abstractions.Exceptions; + +public record ExceptionResponse(Error Error, HttpStatusCode HttpStatusCode); +public record Error(string Code, string Message); \ No newline at end of file diff --git a/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs b/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs new file mode 100644 index 0000000..5bfab72 --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Exceptions; + +public interface IExceptionToResponseMapper +{ + ExceptionResponse Map(Exception exception); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Messaging/IMessage.cs b/src/Brewery.Abstractions/Messaging/IMessage.cs new file mode 100644 index 0000000..1f55f52 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/IMessage.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Messaging; + +public interface IMessage +{ + +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs new file mode 100644 index 0000000..89c2545 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs @@ -0,0 +1,12 @@ +using Brewery.Abstractions.Auth; + +namespace Brewery.Abstractions.Messaging; + +public interface IMessagePublisher +{ + Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage; + + Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage where TResult : JsonWebToken; +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs b/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs new file mode 100644 index 0000000..bf5c300 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Messaging; + +public class RabbitMqOptions +{ + public string HostName { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Postgres/PostgresOptions.cs b/src/Brewery.Abstractions/Postgres/PostgresOptions.cs new file mode 100644 index 0000000..004fd11 --- /dev/null +++ b/src/Brewery.Abstractions/Postgres/PostgresOptions.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Postgres; + +public class PostgresOptions +{ + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQuery.cs b/src/Brewery.Abstractions/Queries/IQuery.cs new file mode 100644 index 0000000..cdd0be3 --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQuery.cs @@ -0,0 +1,8 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQuery +{ + +} + +public interface IQuery : IQuery; \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs b/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs new file mode 100644 index 0000000..ccabad4 --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQueryDispatcher +{ + Task QueryAsync(IQuery query); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQueryHandler.cs b/src/Brewery.Abstractions/Queries/IQueryHandler.cs new file mode 100644 index 0000000..5dc5bac --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQueryHandler.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQueryHandler where TQuery : IQuery +{ + Task QueryAsync(TQuery query); +} \ No newline at end of file diff --git a/src/Brewery.Api/AssemblyLoader.cs b/src/Brewery.Api/AssemblyLoader.cs new file mode 100644 index 0000000..62b9c87 --- /dev/null +++ b/src/Brewery.Api/AssemblyLoader.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace Brewery.Api; + +public static class AssemblyLoader +{ + public static List GetAssemblies() + { + var assemblies = AppDomain.CurrentDomain + .GetAssemblies() + .OrderBy(assembly => assembly.Location) + .ToList(); + + var locations = assemblies + .Where(a => !a.IsDynamic) + .Select(a => a.Location) + .ToArray(); + + var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") + .Where(f => !locations.Contains(f, StringComparer.InvariantCultureIgnoreCase)) + .ToList(); + + files.ForEach(f => assemblies.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(f)))); + + return assemblies; + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Brewery.Api.csproj b/src/Brewery.Api/Brewery.Api.csproj new file mode 100644 index 0000000..3ef5b5f --- /dev/null +++ b/src/Brewery.Api/Brewery.Api.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest new file mode 100644 index 0000000..d35604b --- /dev/null +++ b/src/Brewery.Api/BreweryApi.rest @@ -0,0 +1,143 @@ +@url = http://localhost:5000 + +@beerId = a547da0f-15ef-487a-bf31-b28eccdec50b +@beerId2 = 4fc96048-6ee6-403f-b4c5-647aced09499 +@beerId3 = 356907d4-9d76-4e2f-84c4-3deb5518d78d +@beerStockId = 4f79410c-ce39-47d3-91f3-c726a26aaba4 +@brewerId = 21281174-0a8b-4097-badf-603bab276b9c +@breweryId = 1d91c9f4-61df-4787-9ed6-aa065c2ba8bc +@wholesalerId = c8c4d2a4-a85d-416b-b052-dc1f331f2237 +@beerQuoteId = f6f00152-986d-441c-8662-27e01e279a8e + +@accessToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ZmJiNWU4MC1kNTNmLTQwODMtYTE3OC0wOTYxYzIzZTAyODkiLCJ1bmlxdWVfbmFtZSI6IjZmYmI1ZTgwLWQ1M2YtNDA4My1hMTc4LTA5NjFjMjNlMDI4OSIsImp0aSI6ImVmMDhiMDNkLTg4ZTItNDFjNS04NWIyLTU3YTFhNzZmOTA1YSIsImlhdCI6IjE3MzIwMTMxOTA1NzYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJ1c2VyIiwicGVybWlzc2lvbnMiOlsiYnJld2VyeSIsImJyZXdlciIsImJlZXIiXSwibmJmIjoxNzMyMDEzMTkwLCJleHAiOjE3Mzk3ODkxOTAsImlzcyI6ImJyZXdlcnkifQ.DTOkX2V3KTzeDFzBUFywHNGXp8Og_RpC1QSOUt0q2t8 + +### testEnd +GET {{url}}/testEnd + +### Sign Up +POST {{url}}/account/signUp +Content-Type: application/json + +{ + "email": "user@brewery.com", + "password": "secretBeer", + "role": "user", + "claims": { "permissions": ["brewery", "brewer", "beer"] } +} + +### Sign In +POST {{url}}/account/signIn +Content-Type: application/json + +{ + "email": "user@brewery.com", + "password": "secret" +} + +### Get Account Info +GET {{url}}/account +//Authorization: Bearer {{accessToken}} + +### List all beers by brewery +GET {{url}}/brewery/{{breweryId}}/beers + +### +GET {{url}}/beer/{{beerId}} + +### Brewer adds beer +POST {{url}}/beer +Content-Type: application/json + +{ + "brewerId": "{{brewerId}}", + "name": "beer 3" +} + +### Brewer updates beer +PUT {{url}}/beer/{{beerId3}} +Content-Type: application/json + +{ + "brewerId": "{{brewerId}}", + "name": "super beer 3" +} + +### Brewer deletes beer +DELETE {{url}}/beer/{{beerId3}} +Content-Type: application/json + +{ + "brewerId": "{{brewerId}}" +} + +### Brewer adds beerStock +POST {{url}}/beerstock/{{brewerId}} +Content-Type: application/json + +{ + "beerId": "{{beerId2}}", + "quantity": 100, + "unitPrice": 5 +} + +### +GET {{url}}/brewer/{{brewerId}} + +### Add brewer +POST {{url}}/brewer +Content-Type: application/json + +{ + "name": "brewski 1", + "breweryId": "{{breweryId}}" +} + +### +GET {{url}}/brewery/{{breweryId}} + +### +GET {{url}}/brewery/{{breweryId}}/beers + +### Add Brewery +POST {{url}}/brewery +Content-Type: application/json +Authorization: Bearer {{accessToken}} + +{ + "name": "brewery with Auth" +} + +//Add wholesaler +### +POST {{url}}/wholesaler +Content-Type: application/json + +{ + "name": "wholesaler 1" +} + +//Add sale to wholesaler +### +POST {{url}}/wholesaler/{{wholesalerId}}/sale +Content-Type: application/json + +{ + "beerId": "{{beerId2}}", + "quantity": 50 +} + +### +GET {{url}}/beerQuote/{{beerQuoteId}} + +//Request quote +### +POST {{url}}/beerQuote +Content-Type: application/json + +{ + "wholesalerId": "{{wholesalerId}}", + "beersEnquiry": [ + { "beerId": "{{beerId}}", "requiredQuantity": 10 }, + { "beerId": "{{beerId2}}", "requiredQuantity": 100 } + ] +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/AccountController.cs b/src/Brewery.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..559dc5d --- /dev/null +++ b/src/Brewery.Api/Controllers/AccountController.cs @@ -0,0 +1,54 @@ +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Contexts; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class AccountController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly ICommandHandler _signInCommandHandler; + private readonly IQueryDispatcher _queryDispatcher; + private readonly IContext _context; + + public AccountController(ICommandDispatcher commandDispatcher, + ICommandHandler signInCommandHandler, + IQueryDispatcher queryDispatcher, + IContext context) + { + _commandDispatcher = commandDispatcher; + _signInCommandHandler = signInCommandHandler; + _queryDispatcher = queryDispatcher; + _context = context; + } + + [HttpGet] + //[Authorize] + public async Task> GetUser() + => OkOrNotFound(await _queryDispatcher.QueryAsync(new GetUser(_context.IdentityContext.Id))); + + [HttpPost("signUp")] + public async Task SignIn(CreateAccount command) + { + await _commandDispatcher.DispatchAsync(command with { Id = Guid.NewGuid() }); + return NoContent(); + } + + [HttpPost("signIn")] + public async Task> SignIn(SignIn command) + => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); + + // [HttpPost("signIn")] + // public async Task SignIn(SignIn command) + // { + // await _commandDispatcher.DispatchAsync(command); + // return Ok(); + // } + +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BaseController.cs b/src/Brewery.Api/Controllers/BaseController.cs new file mode 100644 index 0000000..076369f --- /dev/null +++ b/src/Brewery.Api/Controllers/BaseController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +[ApiController] +[Route("/[controller]")] +public class BaseController : ControllerBase +{ + public ActionResult OkOrNotFound(TModel model) + { + if (model is null) + { + return NotFound(); + } + + return Ok(model); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerController.cs b/src/Brewery.Api/Controllers/BeerController.cs new file mode 100644 index 0000000..8b95a33 --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerController.cs @@ -0,0 +1,46 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BeerController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{beerId:guid}")] + public async Task> GetBeer(Guid beerId) + => OkOrNotFound(await _queryDispatcher.QueryAsync(new GetBeer(beerId))); + + [HttpPost] + public async Task AddBeer(AddBeer command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(GetBeer), new { beerId = command.Id }, null); + } + + [HttpPut("{beerId:guid}")] + public async Task UpdateBeer(UpdateBeer command, Guid beerId) + { + await _commandDispatcher.DispatchAsync(command with { Id = beerId }); + return NoContent(); + } + + [HttpDelete("{beerId:guid}")] + public async Task DeleteBeer(DeleteBeer command, Guid beerId) + { + await _commandDispatcher.DispatchAsync(command with { BeerId = beerId } ); + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerQuoteController.cs b/src/Brewery.Api/Controllers/BeerQuoteController.cs new file mode 100644 index 0000000..cd77221 --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerQuoteController.cs @@ -0,0 +1,32 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerQuoteController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BeerQuoteController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{beerQuoteId}")] + public async Task> Get(Guid beerQuoteId) + => Ok(await _queryDispatcher.QueryAsync(new GetBeerQuote(beerQuoteId))); + + [HttpPost] + public async Task Post(RequestQuote command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { beerQuoteId = command.RequestQuoteId }, null); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerStockController.cs b/src/Brewery.Api/Controllers/BeerStockController.cs new file mode 100644 index 0000000..14b5f57 --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerStockController.cs @@ -0,0 +1,23 @@ +using System.Security.Cryptography; +using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerStockController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + + public BeerStockController(ICommandDispatcher commandDispatcher) + { + _commandDispatcher = commandDispatcher; + } + + [HttpPost("{brewerId:guid}")] + public async Task Post(AddBeerStock addBeerStock, Guid brewerId) + { + await _commandDispatcher.DispatchAsync(addBeerStock with { BrewerId = brewerId }); + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BrewerController.cs b/src/Brewery.Api/Controllers/BrewerController.cs new file mode 100644 index 0000000..40611a4 --- /dev/null +++ b/src/Brewery.Api/Controllers/BrewerController.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BrewerController(ICommandDispatcher commandDispatcher, IQueryDispatcher queryDispatcher) + : BaseController +{ + [HttpGet("{brewerId:guid}")] + public async Task> Get(Guid brewerId) + => Ok(await queryDispatcher.QueryAsync(new GetBrewer(brewerId))); + + [HttpPost] + public async Task AddBrewer(AddBrewer command) + { + await commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { brewerId = command.Id }, null); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs new file mode 100644 index 0000000..6799ff3 --- /dev/null +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -0,0 +1,46 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BreweryController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BreweryController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{breweryId:guid}")] + public async Task> Get(Guid breweryId) + { + var brewery = await _queryDispatcher.QueryAsync(new GetBrewery(breweryId)); + return OkOrNotFound(brewery); + } + + [Authorize] + [HttpGet("{breweryId:guid}/beers")] + public async Task>> Browse(Guid breweryId) + { + var beersDto = await _queryDispatcher + .QueryAsync(new BrowseBeersByBrewery(breweryId)); + return Ok(beersDto); + } + + [HttpPost] + [Authorize] + public async Task Post(AddBrewery command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { breweryId = command.Id }, null); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/WholesalerController.cs b/src/Brewery.Api/Controllers/WholesalerController.cs new file mode 100644 index 0000000..39e7265 --- /dev/null +++ b/src/Brewery.Api/Controllers/WholesalerController.cs @@ -0,0 +1,40 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class WholesalerController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public WholesalerController(ICommandDispatcher commandDispatcher, IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{wholesalerId:guid}")] + public async Task> Get(Guid wholesalerId) + { + return Ok(await _queryDispatcher.QueryAsync(new GetWholesaler(wholesalerId))); + } + + [HttpPost] + public async Task Post(AddWholesaler command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { wholesalerId = command.Id }, null); + } + + [HttpPost("{wholesalerId:guid}/sale")] + public async Task AddSale(AddBeerSale command, Guid wholesalerId) + { + await _commandDispatcher.DispatchAsync(command with { WholesalerId = wholesalerId }); + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Program.cs b/src/Brewery.Api/Program.cs new file mode 100644 index 0000000..e155b61 --- /dev/null +++ b/src/Brewery.Api/Program.cs @@ -0,0 +1,21 @@ +using Brewery.Api; +using Brewery.Infrastructure; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Primitives; +using Results = Microsoft.AspNetCore.Http.Results; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var assemblies = AssemblyLoader.GetAssemblies(); + builder.Services.AddInfrastructure(assemblies); + + var app = builder.Build(); + app.UseInfrastructure(); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Properties/launchSettings.json b/src/Brewery.Api/Properties/launchSettings.json new file mode 100644 index 0000000..f3973c1 --- /dev/null +++ b/src/Brewery.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:37980", + "sslPort": 44349 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "/", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Brewery.Api/appsettings.Development.json b/src/Brewery.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Brewery.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json new file mode 100644 index 0000000..99b2c3f --- /dev/null +++ b/src/Brewery.Api/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "postgres": { + "connectionString": "Host=localhost;Database=Brewery;Username=postgres;Password=czcz" + }, + "auth" : { + "IssuerSigningKey": "reaaaureurewruhewurewebnf89432483hgviufdsg8wre8hg", + "issuer": "brewery.identity.service", + "validIssuer": "brewery.identity.service", + "validateIssuer": true, + "validateAudience": false, + "validateLifetime": true, + "expiry": "90:00:00" + }, + "rabbitMq": { + "hostName": "localhost" + } +} diff --git a/src/Brewery.Api/appsettings.test.json b/src/Brewery.Api/appsettings.test.json new file mode 100644 index 0000000..60ac416 --- /dev/null +++ b/src/Brewery.Api/appsettings.test.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "postgres": { + "connectionString": "Host=localhost;Database=Brewery-test;Username=postgres;Password=czcz" + } +} diff --git a/src/Brewery.Application/Brewery.Application.csproj b/src/Brewery.Application/Brewery.Application.csproj new file mode 100644 index 0000000..35d10de --- /dev/null +++ b/src/Brewery.Application/Brewery.Application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Brewery.Application/Commands/AddBeer.cs b/src/Brewery.Application/Commands/AddBeer.cs new file mode 100644 index 0000000..98a3583 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeer.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeer(Guid BrewerId, string Name) : ICommand +{ + public Guid Id { get; set; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerSale.cs b/src/Brewery.Application/Commands/AddBeerSale.cs new file mode 100644 index 0000000..b0fc8f6 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeerSale.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeerSale(Guid WholesalerId, Guid BeerId, int Quantity) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerStock.cs b/src/Brewery.Application/Commands/AddBeerStock.cs new file mode 100644 index 0000000..4c78a54 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeerStock.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeerStock(Guid BeerId, Guid BrewerId, int Quantity, decimal UnitPrice) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBrewer.cs b/src/Brewery.Application/Commands/AddBrewer.cs new file mode 100644 index 0000000..c814f5a --- /dev/null +++ b/src/Brewery.Application/Commands/AddBrewer.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBrewer(string Name, Guid BreweryId) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBrewery.cs b/src/Brewery.Application/Commands/AddBrewery.cs new file mode 100644 index 0000000..0f15023 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBrewery.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBrewery(string Name) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddWholesaler.cs b/src/Brewery.Application/Commands/AddWholesaler.cs new file mode 100644 index 0000000..d0b90dd --- /dev/null +++ b/src/Brewery.Application/Commands/AddWholesaler.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddWholesaler(string Name) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/CreateAccount.cs b/src/Brewery.Application/Commands/CreateAccount.cs new file mode 100644 index 0000000..85b8c7f --- /dev/null +++ b/src/Brewery.Application/Commands/CreateAccount.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record CreateAccount(Guid Id, string Email, string Password, string Role, + Dictionary> Claims) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/DeleteBeer.cs b/src/Brewery.Application/Commands/DeleteBeer.cs new file mode 100644 index 0000000..00ecc0a --- /dev/null +++ b/src/Brewery.Application/Commands/DeleteBeer.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record DeleteBeer(Guid BeerId, Guid BrewerId) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs new file mode 100644 index 0000000..30400e1 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs @@ -0,0 +1,38 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerHandler : ICommandHandler +{ + private readonly IBeerRepository _beerRepository; + private readonly IBrewerRepository _brewerRepository; + + public AddBeerHandler(IBeerRepository beerRepository, + IBrewerRepository brewerRepository) + { + _beerRepository = beerRepository; + _brewerRepository = brewerRepository; + } + + public async Task HandleAsync(AddBeer command) + { + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + + var beer = await _beerRepository.GetBeerById(command.Id); + if (beer is not null) + { + throw new BeerAlreadyExistException(command.Id); + } + + beer = Beer.Create(command.Id, brewer.Id, command.Name); + + await _beerRepository.AddAsync(beer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs new file mode 100644 index 0000000..f142b8c --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs @@ -0,0 +1,60 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerSaleHandler : ICommandHandler +{ + private readonly IWholesalerRepository _wholesalerRepository; + private readonly IBeerStockRepository _beerStockRepository; + private readonly IBeerSaleRepository _beerSaleRepository; + + public AddBeerSaleHandler(IWholesalerRepository wholesalerRepository, + IBeerStockRepository beerStockRepository, + IBeerSaleRepository beerSaleRepository) + { + _wholesalerRepository = wholesalerRepository; + _beerStockRepository = beerStockRepository; + _beerSaleRepository = beerSaleRepository; + } + + public async Task HandleAsync(AddBeerSale command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); + if (wholesaler is null) + { + throw new WholesalerNotFoundException(command.WholesalerId); + } + + var beerStock = await _beerStockRepository.GetBeerStock(command.BeerId); + if (beerStock is null) + { + throw new BeerStockNotFoundException(command.BeerId); + } + + var beerSales = await _beerSaleRepository.BrowseByBeerId(command.BeerId); + var beerSale = beerSales?.SingleOrDefault(b => b.WholesalerId == wholesaler.Id); + if (beerSale is not null) + { + throw new BeerSaleAlreadyExistException(command.WholesalerId, command.BeerId); + } + + if (beerStock.Quantity < command.Quantity) + { + throw new NotEnoughBeerForRequestException(command.BeerId, command.Quantity); + } + + beerSale = BeerSale.Create(Guid.NewGuid(), beerStock.BeerId, + wholesaler.Id, command.Quantity, beerStock.UnitPrice); + + beerStock.TakeForBeerSale(beerSale.Quantity); + await _beerStockRepository.UpdateBeerStock(beerStock); + + //wholesaler.AddBeerSale(beerSale); + await _beerSaleRepository.AddAsync(beerSale); + //await _wholesalerRepository.UpdateWholesaler(wholesaler); + // unit of work pattern? + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs new file mode 100644 index 0000000..2650cd7 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs @@ -0,0 +1,47 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerStockHandler : ICommandHandler +{ + private readonly IBeerStockRepository _beerStockRepository; + private readonly IBrewerRepository _brewerRepository; + private readonly IBeerRepository _beerRepository; + + public AddBeerStockHandler(IBeerStockRepository beerStockRepository, + IBrewerRepository brewerRepository, + IBeerRepository beerRepository) + { + _beerStockRepository = beerStockRepository; + _brewerRepository = brewerRepository; + _beerRepository = beerRepository; + } + + public async Task HandleAsync(AddBeerStock command) + { + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + + var beer = await _beerRepository.GetBeerById(command.BeerId); + if (beer is null) + { + throw new BeerNotFoundException(command.BeerId); + } + + var beerStock = await _beerStockRepository.GetBeerStock(command.BeerId); + if (beerStock is not null) + { + throw new BeerStockAlreadyExistException(command.BeerId); + } + + beerStock = BeerStock.Create(Guid.NewGuid(), brewer.Id, beer.Id, + command.Quantity, command.UnitPrice); + await _beerStockRepository.AddBeerStock(beerStock); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs new file mode 100644 index 0000000..3d94479 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs @@ -0,0 +1,42 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBrewerHandler : ICommandHandler +{ + private readonly IBrewerRepository _brewerRepository; + private readonly IBreweryRepository _breweryRepository; + + public AddBrewerHandler(IBrewerRepository brewerRepository, + IBreweryRepository breweryRepository) + { + _brewerRepository = brewerRepository; + _breweryRepository = breweryRepository; + } + + public async Task HandleAsync(AddBrewer command) + { + var brewer = await _brewerRepository.GetBrewer(command.Id); + if (brewer is not null) + { + throw new BrewerAlreadyExistException(command.Id); + } + + brewer = Brewer.Create(command.Id, command.Name); + if (command.BreweryId != Guid.Empty) + { + var brewery = await _breweryRepository.GetBreweryById(command.BreweryId); + if (brewery is null) + { + throw new BreweryNotFoundException(command.BreweryId); + } + + brewer.ChangeBreweryId(brewery.Id); + } + + await _brewerRepository.AddBrewer(brewer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs new file mode 100644 index 0000000..5aa8c94 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs @@ -0,0 +1,38 @@ +using Brewery.Abstractions.Commands; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBreweryHandler : ICommandHandler +{ + private readonly IBreweryRepository _breweryRepository; + + public AddBreweryHandler(IBreweryRepository breweryRepository) + { + _breweryRepository = breweryRepository; + } + + public async Task HandleAsync(AddBrewery command) + { + var brewery = await _breweryRepository.GetBreweryById(command.Id); + if (brewery is not null) + { + + } + + brewery = Domain.Entities.Brewery.Create(command.Id, command.Name); + await _breweryRepository.AddBrewery(brewery); + } + + public async Task Handle(AddBrewery request, CancellationToken cancellationToken) + { + var brewery = await _breweryRepository.GetBreweryById(request.Id); + if (brewery is not null) + { + + } + + brewery = Domain.Entities.Brewery.Create(request.Id, request.Name); + await _breweryRepository.AddBrewery(brewery); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs new file mode 100644 index 0000000..ec1d624 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs @@ -0,0 +1,28 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddWholesalerHandler : ICommandHandler +{ + private readonly IWholesalerRepository _wholesalerRepository; + + public AddWholesalerHandler(IWholesalerRepository wholesalerRepository) + { + _wholesalerRepository = wholesalerRepository; + } + + public async Task HandleAsync(AddWholesaler command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.Id); + if (wholesaler is not null) + { + throw new WholesaleAlreadyExistException(command.Id); + } + + wholesaler = Wholesaler.Create(command.Id, command.Name); + await _wholesalerRepository.AddWholesaler(wholesaler); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs b/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs new file mode 100644 index 0000000..449ffda --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs @@ -0,0 +1,40 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.AspNetCore.Identity; + +namespace Brewery.Application.Commands.Handlers; + +public class CreateAccountHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly IPasswordHasher _passwordHasher; + + public CreateAccountHandler(IUserRepository userRepository, + IPasswordHasher passwordHasher) + { + _userRepository = userRepository; + _passwordHasher = passwordHasher; + } + + public async Task HandleAsync(CreateAccount command) + { + var user = await _userRepository.GetUserByEmail(command.Email); + if (user is not null) + { + throw new EmailInUseException(command.Email); + } + + var hashedPassword = _passwordHasher.HashPassword(default, command.Password); + user = new User( + command.Id, + command.Email, + hashedPassword, + command.Role is not null ? command.Role : "user", + command.Claims is not null ? command.Claims : new Dictionary>(), + DateTime.UtcNow); + + await _userRepository.AddAsync(user); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs new file mode 100644 index 0000000..ed7b213 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs @@ -0,0 +1,31 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class DeleteBeerHandler : ICommandHandler +{ + private readonly IBeerRepository _beerRepository; + + public DeleteBeerHandler(IBeerRepository beerRepositry) + { + _beerRepository = beerRepositry; + } + + public async Task HandleAsync(DeleteBeer command) + { + var beer = await _beerRepository.GetBeerById(command.BeerId); + if (beer is null) + { + throw new BeerNotFoundException(command.BeerId); + } + + if (beer.BrewerId != command.BrewerId) + { + throw new BeerDoesNotBelongToBrewerException(beer.Id, beer.BrewerId); + } + + await _beerRepository.DeleteAsync(beer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs b/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs new file mode 100644 index 0000000..cb07d4f --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs @@ -0,0 +1,78 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Brewery.Domain.ValueObjects; + +namespace Brewery.Application.Commands.Handlers; + +public class RequestQuoteHandler : ICommandHandler +{ + private readonly HashSet _beerIds = new(); + private readonly IBeerSaleRepository _beerSaleRepository; + private readonly IWholesalerRepository _wholesalerRepository; + private readonly IBeerQuoteRepository _beerQuoteRepository; + + public RequestQuoteHandler(IWholesalerRepository wholesalerRepository, + IBeerSaleRepository beerSaleRepository, + IBeerQuoteRepository beerQuoteRepository) + { + _wholesalerRepository = wholesalerRepository; + _beerSaleRepository = beerSaleRepository; + _beerQuoteRepository = beerQuoteRepository; + } + + public async Task HandleAsync(RequestQuote command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); + if (wholesaler is null) throw new WholesalerNotFoundException(command.WholesalerId); + + CheckForDuplicates(command); + + var beerQuote = BeerQuote.Create(command.RequestQuoteId); + foreach (var enquiry in command.BeersEnquiry) + { + var beerSales = await _beerSaleRepository.BrowseByBeerId(enquiry.BeerId); + var beerSale = beerSales?.SingleOrDefault(b => b.WholesalerId == wholesaler.Id); + if (beerSale is null) + { + throw new WholesalerBeerSaleNotFoundException(wholesaler.Id, enquiry.BeerId); + } + + if (enquiry.RequiredQuantity <= 0) + { + throw new OrderCannotBeEmptyException(enquiry.BeerId); + } + + if (beerSale.Quantity < enquiry.RequiredQuantity) + { + throw new NotEnoughBeerForRequestException(beerSale.BeerId, enquiry.RequiredQuantity); + } + + var beerOrder = new BeerOrder(beerSale.BeerId, enquiry.RequiredQuantity, beerSale.UnitPrice); + beerQuote.AddBeerOrder(beerOrder); + beerSale.SellBeer(beerOrder.Quantity); + } + + beerQuote.CalculateDiscount(); + beerQuote.CalculateTotal(); + await _beerQuoteRepository.AddAsync(beerQuote); + } + + private void CheckForDuplicates(RequestQuote command) + { + _beerIds.Clear(); + foreach (var beerEnquiry in command.BeersEnquiry) + { + var beerId = beerEnquiry.BeerId; + var exists = _beerIds.TryGetValue(beerId, out _); + if (exists) + { + throw new DuplicatesInRequestQuoteException(); + } + + _beerIds.Add(beerId); + + } + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs new file mode 100644 index 0000000..049f73c --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs @@ -0,0 +1,25 @@ +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Messaging; + +namespace Brewery.Application.Commands.Handlers; + +public class SignInHandler : ICommandHandler +{ + private readonly IMessagePublisher _messagePublisher; + public SignInHandler(IMessagePublisher messagePublisher) + { + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(SignIn command) + { + var jwt = await _messagePublisher + .PublishAsync(command, "brewery_id_service_exchange"); + + // var jwt = await _messagePublisher + // .PublishAsync(command, "brewery_id_service_exchange"); + + return jwt; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs new file mode 100644 index 0000000..564a67f --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs @@ -0,0 +1,46 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public record UpdateBeerHandler : ICommandHandler +{ + private readonly IBeerRepository _beerRepository; + private readonly IBrewerRepository _brewerRepository; + + public UpdateBeerHandler(IBeerRepository beerRepository, + IBrewerRepository brewerRepository) + { + _beerRepository = beerRepository; + _brewerRepository = brewerRepository; + } + + public async Task HandleAsync(UpdateBeer command) + { + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + + var beer = await _beerRepository.GetBeerById(command.Id); + if (beer is null) + { + throw new BeerNotFoundException(command.Id); + } + + if (beer.BrewerId != brewer.Id) + { + throw new BeerDoesNotBelongToBrewerException(beer.Id, brewer.Id); + } + + if (!string.IsNullOrEmpty(command.Name)) + { + beer.ChangeName(command.Name); + } + + await _beerRepository.UpdateAsync(beer); + } +} + diff --git a/src/Brewery.Application/Commands/RequestQuote.cs b/src/Brewery.Application/Commands/RequestQuote.cs new file mode 100644 index 0000000..2dbcd6b --- /dev/null +++ b/src/Brewery.Application/Commands/RequestQuote.cs @@ -0,0 +1,9 @@ +using Brewery.Abstractions.Commands; +using Brewery.Domain.ValueObjects; + +namespace Brewery.Application.Commands; + +public record RequestQuote(Guid WholesalerId, IEnumerable BeersEnquiry) : ICommand +{ + public Guid RequestQuoteId { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/SignIn.cs b/src/Brewery.Application/Commands/SignIn.cs new file mode 100644 index 0000000..79dc300 --- /dev/null +++ b/src/Brewery.Application/Commands/SignIn.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record SignIn(string Email, string Password) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/UpdateBeer.cs b/src/Brewery.Application/Commands/UpdateBeer.cs new file mode 100644 index 0000000..671119c --- /dev/null +++ b/src/Brewery.Application/Commands/UpdateBeer.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record UpdateBeer(Guid Id, Guid BrewerId, string Name = null, decimal UnitPrice = default) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/DTO/AccountDto.cs b/src/Brewery.Application/DTO/AccountDto.cs new file mode 100644 index 0000000..e6705b6 --- /dev/null +++ b/src/Brewery.Application/DTO/AccountDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class AccountDto +{ + public Guid UserId { get; set; } + public string Email { get; set; } + public string Role { get; set; } + public Dictionary> Claims { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerDto.cs b/src/Brewery.Application/DTO/BeerDto.cs new file mode 100644 index 0000000..908a612 --- /dev/null +++ b/src/Brewery.Application/DTO/BeerDto.cs @@ -0,0 +1,10 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Application.DTO; + +public class BeerDto +{ + public Guid Id { get; set; } + public Guid BrewerId { get; set; } + public string Name { get; set; } +} diff --git a/src/Brewery.Application/DTO/BeerOrderDto.cs b/src/Brewery.Application/DTO/BeerOrderDto.cs new file mode 100644 index 0000000..c4df59f --- /dev/null +++ b/src/Brewery.Application/DTO/BeerOrderDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class BeerOrderDto +{ + public Guid BeerId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public decimal Total { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerQuoteDto.cs b/src/Brewery.Application/DTO/BeerQuoteDto.cs new file mode 100644 index 0000000..315a82f --- /dev/null +++ b/src/Brewery.Application/DTO/BeerQuoteDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class BeerQuoteDto +{ + public Guid Id { get; set; } + public IEnumerable BeerOrders { get; set; } + public int DiscountInPercent { get; set; } + public decimal Total { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BrewerDto.cs b/src/Brewery.Application/DTO/BrewerDto.cs new file mode 100644 index 0000000..bf57bbc --- /dev/null +++ b/src/Brewery.Application/DTO/BrewerDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class BrewerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BreweryDto.cs b/src/Brewery.Application/DTO/BreweryDto.cs new file mode 100644 index 0000000..7a5a44a --- /dev/null +++ b/src/Brewery.Application/DTO/BreweryDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class BreweryDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/Extensions.cs b/src/Brewery.Application/DTO/Extensions.cs new file mode 100644 index 0000000..30915b1 --- /dev/null +++ b/src/Brewery.Application/DTO/Extensions.cs @@ -0,0 +1,56 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.ValueObjects; + +namespace Brewery.Application.DTO; + +public static class Extensions +{ + public static BeerDto AsDto(this Beer beer) + { + var beerDto = new BeerDto + { + Id = beer.Id, + BrewerId = beer.BrewerId, + Name = beer.Name, + }; + + return beerDto; + } + + public static BrewerDto AsDto(this Brewer brewer) + { + var brewerDto = new BrewerDto + { + Id = brewer.Id, + Name = brewer.Name, + }; + + return brewerDto; + } + + public static WholesalerDto AsDto(this Wholesaler wholesaler) + => new WholesalerDto + { + Id = wholesaler.Id, + Name = wholesaler.Name, + }; + + public static BeerOrderDto AsDto(this BeerOrder beerOrder) + => new BeerOrderDto + { + BeerId = beerOrder.BeerId, + Quantity = beerOrder.Quantity, + UnitPrice = beerOrder.UnitPrice, + Total = beerOrder.Total, + }; + + public static BeerQuoteDto AsDto(this BeerQuote beerQuote) + => new BeerQuoteDto() + { + Id = beerQuote.Id, + BeerOrders = beerQuote.BeerOrders + .Select(bo => bo.AsDto()), + Total = beerQuote.Total, + DiscountInPercent = beerQuote.DiscountInPercent, + }; +} diff --git a/src/Brewery.Application/DTO/WholesalerDto.cs b/src/Brewery.Application/DTO/WholesalerDto.cs new file mode 100644 index 0000000..4a429ff --- /dev/null +++ b/src/Brewery.Application/DTO/WholesalerDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class WholesalerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs new file mode 100644 index 0000000..51c6ac9 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerAlreadyExistException : BreweryException +{ + public Guid BeerId { get; } + public BeerAlreadyExistException(Guid beerId) + : base($"Beer with id '{beerId}' already exists.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs b/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs new file mode 100644 index 0000000..e96ea12 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerDoesNotBelongToBrewerException : BreweryException +{ + public Guid BeerId { get; } + public Guid BrewerId { get; } + public BeerDoesNotBelongToBrewerException(Guid beerId, Guid brewerId) + : base($"Beer with id {beerId} does not belong to brewer with id '{brewerId}'") + { + BeerId = beerId; + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerNotFoundException.cs new file mode 100644 index 0000000..cee18ba --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerNotFoundException(Guid beerId) + : base($"Beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs new file mode 100644 index 0000000..f6fcbce --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerSaleAlreadyExistException : BreweryException +{ + public Guid WholesalerId { get; } + public Guid BeerId { get; } + public BeerSaleAlreadyExistException(Guid wholesalerId, Guid beerId) + : base($"Wholsaler with id '{wholesalerId}' already offers beer sale with id '{beerId}'.") + { + WholesalerId = wholesalerId; + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs new file mode 100644 index 0000000..07d83fe --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerSaleNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerSaleNotFoundException(Guid beerId) + : base($"Sale for beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs new file mode 100644 index 0000000..f07cdf3 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerStockAlreadyExistException : BreweryException +{ + public Guid BeerId { get; } + public BeerStockAlreadyExistException(Guid beerId) + : base($"Beer stock for beer with id '{beerId}' already exists.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs new file mode 100644 index 0000000..4832853 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerStockNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerStockNotFoundException(Guid beerId) + : base($"Beer stock for beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs new file mode 100644 index 0000000..d01d22c --- /dev/null +++ b/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BrewerAlreadyExistException : BreweryException +{ + public Guid BrewerId { get; } + public BrewerAlreadyExistException(Guid brewerId) + : base($"Brewer with id '{brewerId}' already exists.") + { + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs b/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs new file mode 100644 index 0000000..7cc43e5 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BrewerNotFoundException : BreweryException +{ + public Guid BrewerId { get; } + public BrewerNotFoundException(Guid brewerId) + : base($"Brewer with id '{brewerId}' was not found.") + { + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs b/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs new file mode 100644 index 0000000..4dd090e --- /dev/null +++ b/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BreweryNotFoundException : BreweryException +{ + public Guid BreweryId { get; } + public BreweryNotFoundException(Guid breweryId) + : base($"Brewery with id '{breweryId}' was not found.") + { + BreweryId = breweryId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs b/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs new file mode 100644 index 0000000..4f3ba08 --- /dev/null +++ b/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class DuplicatesInRequestQuoteException : BreweryException +{ + public DuplicatesInRequestQuoteException() + : base($"Request quote cannot contain duplicates.") + { + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/EmailInUseException.cs b/src/Brewery.Application/Exceptions/EmailInUseException.cs new file mode 100644 index 0000000..d0b8b6b --- /dev/null +++ b/src/Brewery.Application/Exceptions/EmailInUseException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class EmailInUseException : BreweryException +{ + public string Email { get; } + public EmailInUseException(string email) + : base($"Email '{email}' already in use.") + { + Email = email; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs b/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs new file mode 100644 index 0000000..59a24bd --- /dev/null +++ b/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class ExcessiveBeerSaleOrderQuantityException : BreweryException +{ + public int Quantity { get; } + public ExcessiveBeerSaleOrderQuantityException(int quantity) + : base($"Order quantity of '{quantity}' beers is excessive. Not enough stock.") + { + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs b/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs new file mode 100644 index 0000000..2dcadae --- /dev/null +++ b/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class NotEnoughBeerForRequestException : BreweryException +{ + public Guid BeerId { get; } + public int Quantity { get; } + public NotEnoughBeerForRequestException(Guid beerId, int quantity) + : base($"Not enough beer with id '{beerId}'. Requested quantity of '{quantity}' is excessive.") + { + BeerId = beerId; + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs b/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs new file mode 100644 index 0000000..10feb22 --- /dev/null +++ b/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class OrderCannotBeEmptyException : BreweryException +{ + public Guid BeerId { get; } + public OrderCannotBeEmptyException(Guid beerId) + : base($"Beer order with id '{beerId}' cannot be empty.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs b/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs new file mode 100644 index 0000000..5db53f8 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesaleAlreadyExistException : BreweryException +{ + public Guid WholesalerId { get; } + public WholesaleAlreadyExistException(Guid wholesalerId) + : base($"Wholesaler with id '{wholesalerId}' already exists.") + { + WholesalerId = wholesalerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs b/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs new file mode 100644 index 0000000..1cf8d77 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesalerBeerSaleNotFoundException : BreweryException +{ + public Guid WholesalerId { get; } + public Guid BeerId { get; } + public WholesalerBeerSaleNotFoundException(Guid wholesalerId, Guid beerId) + : base($"Beer sale was not found. Wholesaler with id '{wholesalerId}' does not offer beer sale with id '{beerId}'.") + { + WholesalerId = wholesalerId; + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs b/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs new file mode 100644 index 0000000..5601a49 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesalerNotFoundException : BreweryException +{ + public Guid WholesalerId { get; } + public WholesalerNotFoundException(Guid wholesalerId) + : base($"Wholesaler with id '{wholesalerId}' was not found.") + { + WholesalerId = wholesalerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs b/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs new file mode 100644 index 0000000..8707c79 --- /dev/null +++ b/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record BrowseBeersByBrewery(Guid BreweryId) : IQuery>; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBeer.cs b/src/Brewery.Application/Queries/GetBeer.cs new file mode 100644 index 0000000..5b64fad --- /dev/null +++ b/src/Brewery.Application/Queries/GetBeer.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBeer(Guid Id) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBeerQuote.cs b/src/Brewery.Application/Queries/GetBeerQuote.cs new file mode 100644 index 0000000..fa6dc0f --- /dev/null +++ b/src/Brewery.Application/Queries/GetBeerQuote.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBeerQuote(Guid BeerQuoteId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBrewer.cs b/src/Brewery.Application/Queries/GetBrewer.cs new file mode 100644 index 0000000..ead9465 --- /dev/null +++ b/src/Brewery.Application/Queries/GetBrewer.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBrewer(Guid BrewerId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBrewery.cs b/src/Brewery.Application/Queries/GetBrewery.cs new file mode 100644 index 0000000..7901def --- /dev/null +++ b/src/Brewery.Application/Queries/GetBrewery.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBrewery(Guid Id) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetUser.cs b/src/Brewery.Application/Queries/GetUser.cs new file mode 100644 index 0000000..50e7ee4 --- /dev/null +++ b/src/Brewery.Application/Queries/GetUser.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetUser(Guid UserId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetWholesaler.cs b/src/Brewery.Application/Queries/GetWholesaler.cs new file mode 100644 index 0000000..f508c49 --- /dev/null +++ b/src/Brewery.Application/Queries/GetWholesaler.cs @@ -0,0 +1,7 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Domain.Entities; + +namespace Brewery.Application.Queries; + +public record GetWholesaler(Guid WholesalerId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Brewery.Domain.csproj b/src/Brewery.Domain/Brewery.Domain.csproj new file mode 100644 index 0000000..8877d85 --- /dev/null +++ b/src/Brewery.Domain/Brewery.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs new file mode 100644 index 0000000..6b50218 --- /dev/null +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -0,0 +1,30 @@ +using Brewery.Abstractions.Exceptions; +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; + +public class Beer +{ + public Guid Id { get; private set; } + public Guid BrewerId { get; private set; } + public string Name { get; private set; } + + public Beer(Guid id, Guid brewerId) + { + Id = id; + BrewerId = brewerId; + } + + public void ChangeName(string name) + { + Name = name; + } + + public static Beer Create(Guid id, Guid brewerId, string name) + { + var beer = new Beer(id, brewerId); + beer.ChangeName(name); + + return beer; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerOrder.cs b/src/Brewery.Domain/Entities/BeerOrder.cs new file mode 100644 index 0000000..0c6587e --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerOrder.cs @@ -0,0 +1,17 @@ +namespace Brewery.Domain.Entities; + +public class BeerOrder +{ + public Guid BeerId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public decimal Total { get; set; } + + public BeerOrder(Guid beerId, int quantity, decimal unitPrice) + { + BeerId = beerId; + Quantity = quantity; + UnitPrice = unitPrice; + Total = quantity * unitPrice; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerQuote.cs b/src/Brewery.Domain/Entities/BeerQuote.cs new file mode 100644 index 0000000..13dcdbe --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerQuote.cs @@ -0,0 +1,55 @@ +using Brewery.Domain.ValueObjects; + +namespace Brewery.Domain.Entities; + +public class BeerQuote +{ + public Guid Id { get; private set; } + public IEnumerable BeerOrders => _beerOrders; + private readonly HashSet _beerOrders = new HashSet(); + public int DiscountInPercent { get; private set; } + public decimal Total { get; private set; } + + public BeerQuote(Guid id) + { + Id = id; + } + + public void AddBeerOrder(BeerOrder beerOrder) + => _beerOrders.Add(beerOrder); + + public void CalculateTotal() + { + foreach (var beerOrder in _beerOrders) + { + Total += beerOrder.Total; + } + + if (DiscountInPercent is not 0) + { + Total = Total * (100 - DiscountInPercent) / 100; + } + } + + public void CalculateDiscount() + { + var beerAmount = _beerOrders.Sum(b => b.Quantity); + if (beerAmount > 20) + { + DiscountInPercent = 20; + return; + } + + if (beerAmount > 10) + { + DiscountInPercent = 10; + return; + } + + DiscountInPercent = 0; + return; + } + + public static BeerQuote Create(Guid id) + => new BeerQuote(id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerSale.cs b/src/Brewery.Domain/Entities/BeerSale.cs new file mode 100644 index 0000000..7696231 --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerSale.cs @@ -0,0 +1,53 @@ +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; + +public class BeerSale +{ + public Guid Id { get; private set; } + public Guid BeerId { get; private set; } + public Guid WholesalerId { get; private set; } + public int Quantity { get; private set; } + public decimal UnitPrice { get; private set; } + + public BeerSale(Guid id, Guid beerId, Guid wholesalerId) + { + Id = id; + BeerId = beerId; + WholesalerId = wholesalerId; + } + + public void RestockBeer(Guid beerId, int quantity) + { + if (beerId != BeerId) + { + throw new InvalidBeerToBeRestockedException(beerId); + } + + if (quantity <= 0) + { + throw new InvalidBeerQuantityException(quantity); + } + + Quantity += quantity; + } + + public void SellBeer(int quantity) + { + if (Quantity < quantity) + { + throw new NotEnoughBeerToTakeException(quantity); + } + + Quantity -= quantity; + } + + public static BeerSale Create(Guid id, Guid beerId, Guid wholesalerId, int quantity, decimal unitPrice) + { + var sale = new BeerSale(id, beerId, wholesalerId); + sale.RestockBeer(beerId, quantity); + sale.UnitPrice = unitPrice; + + return sale; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerStock.cs b/src/Brewery.Domain/Entities/BeerStock.cs new file mode 100644 index 0000000..a04286f --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerStock.cs @@ -0,0 +1,63 @@ +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; + +public class BeerStock +{ + public Guid Id { get; private set; } + public Guid BeerId { get; private set; } + public Guid BrewerId { get; private set; } + public int Quantity { get; private set; } + public decimal UnitPrice { get; private set; } + + public BeerStock(Guid id, Guid brewerId, Guid beerId) + { + Id = id; + BrewerId = brewerId; + BeerId = beerId; + } + + public void RestockBeer(Guid beerId, int quantity) + { + if (beerId != BeerId) + { + throw new InvalidBeerToBeRestockedException(beerId); + } + + if (quantity <= 0) + { + throw new InvalidBeerQuantityException(quantity); + } + + Quantity += quantity; + } + + public void TakeForBeerSale(int quantity) + { + if (Quantity < quantity) + { + throw new NotEnoughBeerToTakeException(quantity); + } + + Quantity -= quantity; + } + + public void SetPrice(decimal unitPrice) + { + if (unitPrice <= 0) + { + throw new InvalidUnitPriceException(unitPrice); + } + + UnitPrice = unitPrice; + } + + public static BeerStock Create(Guid id, Guid brewerId, Guid beerId, int quantity, decimal unitPrice) + { + var beerStock = new BeerStock(id, brewerId, beerId); + beerStock.RestockBeer(beerId, quantity); + beerStock.SetPrice(unitPrice); + + return beerStock; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs new file mode 100644 index 0000000..6a15c9d --- /dev/null +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -0,0 +1,45 @@ +namespace Brewery.Domain.Entities; + +public class Brewer +{ + public IEnumerable Beers => _beers; + public IEnumerable BeerStocks => _beerStocks; + public Guid Id { get; private set; } + public Guid? BreweryId { get; private set; } + public string Name { get; private set; } + private readonly List _beers = new List(); + private readonly List _beerStocks = new List(); + + public Brewer(Guid id) + { + Id = id; + } + + public void ChangeName(string name) + => Name = name; + + public void AddBeer(Beer beer) + => _beers.Add(beer); + + public void DeleteBeer(Beer beer) + => _beers.Remove(beer); + + public void AddBeerStock(BeerStock stock) + => _beerStocks.Add(stock); + + public void DeleteBeerStock(BeerStock stock) + => _beerStocks.Remove(stock); + + public void ChangeBreweryId(Guid breweryId) + { + BreweryId = breweryId; + } + + public static Brewer Create(Guid id, string name) + { + var brewer = new Brewer(id); + brewer.ChangeName(name); + + return brewer; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs new file mode 100644 index 0000000..59a9528 --- /dev/null +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -0,0 +1,38 @@ +namespace Brewery.Domain.Entities; + +public class Brewery +{ + private readonly List _brewers = new List(); + public Guid Id { get; private set; } + public string Name { get; private set; } + public IEnumerable Brewers => _brewers; + + public Brewery(Guid id) + { + Id = id; + } + + public void ChangeName(string name) + { + Name = name; + } + + public void AddBrewer(Brewer brewer) + { + _brewers.Add(brewer); + } + + public void RemoveBrewer(Brewer brewer) + { + _brewers.Remove(brewer); + } + + public static Brewery Create(Guid id, string name) + { + var brewery = new Brewery(id); + brewery.ChangeName(name); + + return brewery; + } + +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/User.cs b/src/Brewery.Domain/Entities/User.cs new file mode 100644 index 0000000..e924b4d --- /dev/null +++ b/src/Brewery.Domain/Entities/User.cs @@ -0,0 +1,22 @@ +namespace Brewery.Domain.Entities; + +public class User +{ + public Guid Id { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string Role { get; set; } + public Dictionary> Claims { get; set; } + public DateTime CreatedAt { get; set; } + + public User(Guid id, string email, string password, string role, + Dictionary> claims, DateTime createdAt) + { + Id = id; + Email = email; + Password = password; + Role = role; + Claims = claims; + CreatedAt = createdAt; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Wholesaler.cs b/src/Brewery.Domain/Entities/Wholesaler.cs new file mode 100644 index 0000000..b9fed76 --- /dev/null +++ b/src/Brewery.Domain/Entities/Wholesaler.cs @@ -0,0 +1,35 @@ +namespace Brewery.Domain.Entities; + +public class Wholesaler +{ + public Guid Id { get; private set; } + public string Name { get; private set; } + private readonly HashSet _beerSales = new(); + public IEnumerable BeerSales => _beerSales; + + public Wholesaler(Guid id) + { + Id = id; + } + + public void ChangeName(string name) + => Name = name; + + public void AddBeerSale(BeerSale beerSale) + { + _beerSales.Add(beerSale); + } + + public void RemoveBeerSale(BeerSale beerSale) + { + _beerSales.Remove(beerSale); + } + + public static Wholesaler Create(Guid id, string name) + { + var wholesaler = new Wholesaler(id); + wholesaler.ChangeName(name); + + return wholesaler; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs b/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs new file mode 100644 index 0000000..4202550 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidBeerQuantityException : BreweryException +{ + public int Quantity { get; } + public InvalidBeerQuantityException(int quantity) + : base($"Quantity of '{quantity}' is invalid.") + { + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs b/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs new file mode 100644 index 0000000..97e8fd8 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidBeerToBeRestockedException : BreweryException +{ + public Guid BeerId { get; } + public InvalidBeerToBeRestockedException(Guid beerId) + : base($"Beer with '{beerId}' is invalid to be restocked.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs b/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs new file mode 100644 index 0000000..1e4f8a4 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidCredentialsException : BreweryException +{ + public InvalidCredentialsException() : base($"Invalid sign-in credentials.") + { + + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs b/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs new file mode 100644 index 0000000..89616da --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidUnitPriceException : BreweryException +{ + public decimal Price { get; } + public InvalidUnitPriceException(decimal price) + : base($"Unit Price of '{price}' is invalid.") + { + Price = price; + } +} diff --git a/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs b/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs new file mode 100644 index 0000000..47b214f --- /dev/null +++ b/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class NotEnoughBeerToTakeException : BreweryException +{ + public NotEnoughBeerToTakeException(int quantity) + : base($"Not enough beer to take. Quantity of '{quantity}' is excessive.") + { + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs b/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs new file mode 100644 index 0000000..16975a5 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs @@ -0,0 +1,9 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerQuoteRepository +{ + Task AddAsync(BeerQuote beerQuote); + Task GetBeerQuote(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBeerRepository.cs b/src/Brewery.Domain/Repositories/IBeerRepository.cs new file mode 100644 index 0000000..2de516a --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerRepository +{ + Task AddAsync(Beer beer); + Task UpdateAsync(Beer beer); + Task DeleteAsync(Beer beer); + Task GetBeerById(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBeerSaleRepository.cs b/src/Brewery.Domain/Repositories/IBeerSaleRepository.cs new file mode 100644 index 0000000..f2c4994 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerSaleRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerSaleRepository +{ + Task AddAsync(BeerSale beerSale); + Task UpdateAsync(BeerSale beerSale); + Task DeleteAsync(BeerSale beerSale); + Task> BrowseByBeerId(Guid beerId); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBeerStockRepository.cs b/src/Brewery.Domain/Repositories/IBeerStockRepository.cs new file mode 100644 index 0000000..1de40ca --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerStockRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerStockRepository +{ + Task AddBeerStock(BeerStock stock); + Task UpdateBeerStock(BeerStock stock); + Task DeleteBeerStock(BeerStock stock); + Task GetBeerStock(Guid beerId); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBrewerRepository.cs b/src/Brewery.Domain/Repositories/IBrewerRepository.cs new file mode 100644 index 0000000..e870953 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBrewerRepository.cs @@ -0,0 +1,9 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBrewerRepository +{ + Task AddBrewer(Brewer brewer); + Task GetBrewer(Guid brewerId); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBreweryRepository.cs b/src/Brewery.Domain/Repositories/IBreweryRepository.cs new file mode 100644 index 0000000..fbb0961 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBreweryRepository.cs @@ -0,0 +1,9 @@ +namespace Brewery.Domain.Repositories; + +public interface IBreweryRepository +{ + Task AddBrewery(Entities.Brewery brewery); + Task UpdateBrewery(Entities.Brewery brewery); + Task DeleteBrewery(Entities.Brewery brewery); + Task GetBreweryById(Guid breweryId); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IUserRepository.cs b/src/Brewery.Domain/Repositories/IUserRepository.cs new file mode 100644 index 0000000..b87bb91 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IUserRepository.cs @@ -0,0 +1,10 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IUserRepository +{ + Task AddAsync(User user); + Task GetUserById(Guid id); + Task GetUserByEmail(string email); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IWholesalerRepository.cs b/src/Brewery.Domain/Repositories/IWholesalerRepository.cs new file mode 100644 index 0000000..8c5f4fb --- /dev/null +++ b/src/Brewery.Domain/Repositories/IWholesalerRepository.cs @@ -0,0 +1,10 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IWholesalerRepository +{ + Task AddWholesaler(Wholesaler wholesaler); + Task UpdateWholesaler(Wholesaler wholesaler); + Task GetWholesaler(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs b/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs new file mode 100644 index 0000000..ceedf79 --- /dev/null +++ b/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs @@ -0,0 +1,7 @@ +namespace Brewery.Domain.ValueObjects; + +public class BeerEnquiry +{ + public Guid BeerId { get; set; } + public int RequiredQuantity { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Auth/AuthManager.cs b/src/Brewery.Infrastructure/Auth/AuthManager.cs new file mode 100644 index 0000000..ec874fc --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/AuthManager.cs @@ -0,0 +1,82 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Brewery.Abstractions.Auth; +using Microsoft.IdentityModel.Tokens; + +namespace Brewery.Infrastructure.Auth; + +public class AuthManager : IAuthManager +{ + private readonly static Dictionary> EmptyClaims = new(); + private readonly AuthOptions _options; + private readonly string _issuer; + private readonly SigningCredentials _signingCredentials; + + public AuthManager(AuthOptions options) + { + _options = options; + _issuer = options.Issuer; + _signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.IssuerSigningKey)), + SecurityAlgorithms.HmacSha256); + } + public JsonWebToken GenerateToken(string userId, string role, + string audience = null, IDictionary> claims = null) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentException("User id claim (subject) cannot be empty", nameof(userId)); + } + + var now = DateTime.UtcNow; + var jwtClaims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.UniqueName, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeMilliseconds().ToString()) + }; + + if (!string.IsNullOrWhiteSpace(role)) + { + jwtClaims.Add(new Claim(ClaimTypes.Role, role)); + } + + if (!string.IsNullOrWhiteSpace(audience)) + { + jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, audience)); + } + + if (claims?.Any() is true) + { + var customClaims = new List(); + foreach (var (claim, values) in claims) + { + customClaims.AddRange(values.Select(v => new Claim(claim, v))); + } + + jwtClaims.AddRange(customClaims); + } + + var expires = now.Add(_options.Expiry); + var jwt = new JwtSecurityToken( + _issuer, + claims: jwtClaims, + notBefore: now, + expires: expires, + signingCredentials: _signingCredentials + ); + + var token = new JwtSecurityTokenHandler().WriteToken(jwt); + + return new JsonWebToken + { + AccessToken = token, + RefreshToken = string.Empty, + Expires = new DateTimeOffset(expires).ToUnixTimeMilliseconds(), + Id = userId, + Role = role ?? string.Empty, + Claims = claims ?? EmptyClaims, + }; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Auth/AuthOptions.cs b/src/Brewery.Infrastructure/Auth/AuthOptions.cs new file mode 100644 index 0000000..e07b5d5 --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/AuthOptions.cs @@ -0,0 +1,12 @@ +namespace Brewery.Infrastructure.Auth; + +public class AuthOptions +{ + public string IssuerSigningKey { get; set; } + public string Issuer { get; set; } + public string ValidIssuer { get; set; } + public bool ValidateIssuer { get; set; } + public bool ValidateAudience { get; set; } + public bool ValidateLifetime { get; set; } + public TimeSpan Expiry { get; set; } +} diff --git a/src/Brewery.Infrastructure/Auth/Extensions.cs b/src/Brewery.Infrastructure/Auth/Extensions.cs new file mode 100644 index 0000000..bee1d08 --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/Extensions.cs @@ -0,0 +1,53 @@ +using System.Text; +using Brewery.Abstractions.Auth; +using Brewery.Domain.Entities; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace Brewery.Infrastructure.Auth; + +public static class Extensions +{ + public static IServiceCollection AddAuth(this IServiceCollection services) + { + var authOptions = services.GetOptions("auth"); + services.AddSingleton(); + services.AddSingleton, PasswordHasher>(); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = authOptions.ValidIssuer, + ValidateIssuer = authOptions.ValidateIssuer, + ValidateAudience = authOptions.ValidateAudience, + ValidateLifetime = authOptions.ValidateLifetime, + ClockSkew = TimeSpan.Zero + + }; + if (string.IsNullOrWhiteSpace(authOptions.IssuerSigningKey)) + { + throw new ArgumentException("Missing issuer signing key.", nameof(authOptions.IssuerSigningKey)); + } + + var rawKey = Encoding.UTF8.GetBytes(authOptions.IssuerSigningKey); + tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(rawKey); + + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(o => + { + o.TokenValidationParameters = tokenValidationParameters; + }); + + services.AddSingleton(authOptions); + services.AddSingleton(tokenValidationParameters); + + services.AddAuthorization(); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj new file mode 100644 index 0000000..0f40ace --- /dev/null +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + + + + + + + + + + + + + + + + + diff --git a/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs b/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs new file mode 100644 index 0000000..5d00eae --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs @@ -0,0 +1,23 @@ +using Brewery.Abstractions.Commands; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Commands; + +public class CommandDispatcher : ICommandDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public CommandDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task DispatchAsync(TCommand command) where TCommand : class, ICommand + { + using var scope = _serviceProvider.CreateScope(); + var handler = scope.ServiceProvider + .GetService>(); + + await handler.HandleAsync(command); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs b/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs new file mode 100644 index 0000000..a0de504 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs @@ -0,0 +1,7 @@ +namespace Brewery.Infrastructure.Commands.Decorated; + +[AttributeUsage(AttributeTargets.Class)] +public class DecoratorAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs b/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs new file mode 100644 index 0000000..47bcf65 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Brewery.Infrastructure.EF.UnitOfWork; + +namespace Brewery.Infrastructure.Commands.Decorated; + +[Decorator] +public class UnitOfWorkDecoratedAddBeerSaleHandler : ICommandHandler +{ + private readonly ICommandHandler _commandHandler; + private readonly BreweryUnitOfWork _breweryUnitOfWork; + + public UnitOfWorkDecoratedAddBeerSaleHandler(ICommandHandler commandHandler, + BreweryUnitOfWork breweryUnitOfWork) + { + _commandHandler = commandHandler; + _breweryUnitOfWork = breweryUnitOfWork; + } + + public async Task HandleAsync(AddBeerSale command) + { + await _breweryUnitOfWork.ExecuteAsync(() => _commandHandler.HandleAsync(command)); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs new file mode 100644 index 0000000..0f2494f --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Brewery.Application.Commands.Handlers; +using Brewery.Infrastructure.Commands.Decorated; +using Brewery.Infrastructure.EF.UnitOfWork; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Commands; + +public static class Extensions +{ + public static IServiceCollection AddCommands(this IServiceCollection services, IList assemblies) + { + services.AddSingleton(); + + services.Scan(a => a.FromAssemblies(assemblies) + .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>)) + .WithoutAttribute()) + .AsImplementedInterfaces() + .WithScopedLifetime()); + services.AddScoped, SignInHandler>(); + services.TryDecorate, UnitOfWorkDecoratedAddBeerSaleHandler>(); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/Context.cs b/src/Brewery.Infrastructure/Contexts/Context.cs new file mode 100644 index 0000000..e3a284e --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/Context.cs @@ -0,0 +1,30 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Contexts; + +public class Context : IContext +{ + public string RequestId { get; } = $"{Guid.NewGuid():N}"; + public string TraceId { get; } + public IIdentityContext IdentityContext { get; } + + internal Context() + { + } + + public Context(IHttpContextAccessor httpContextAccessor) : + this(httpContextAccessor.HttpContext.TraceIdentifier, + new IdentityContext(httpContextAccessor.HttpContext.User)) + { + + } + + public Context(string traceId, IIdentityContext identityContext) + { + TraceId = traceId; + IdentityContext = identityContext; + } + + public static Context Empty() => new Context(); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/ContextFactory.cs b/src/Brewery.Infrastructure/Contexts/ContextFactory.cs new file mode 100644 index 0000000..55b46e8 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/ContextFactory.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Contexts; + +public class ContextFactory : IContextFactory +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public ContextFactory(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public IContext Create() + { + if (_httpContextAccessor.HttpContext is null) + { + return Context.Empty(); + } + + return new Context(_httpContextAccessor); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/Extensions.cs b/src/Brewery.Infrastructure/Contexts/Extensions.cs new file mode 100644 index 0000000..dca42d6 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/Extensions.cs @@ -0,0 +1,17 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Contexts; + +public static class Extensions +{ + public static IServiceCollection AddContexts(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(sp => sp.GetRequiredService().Create()); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/IdentityContext.cs b/src/Brewery.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 0000000..781c0d6 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using Brewery.Abstractions.Contexts; + +namespace Brewery.Infrastructure.Contexts; + +public class IdentityContext : IIdentityContext +{ + public bool IsAuthenticated { get; } + public Guid Id { get; } + public string Role { get; } + public Dictionary> Claims { get; } + + public IdentityContext(ClaimsPrincipal principal) + { + IsAuthenticated = principal.Identity?.IsAuthenticated is true; + Id = IsAuthenticated ? Guid.Parse(principal.Identity.Name) : Guid.Empty; + Role = IsAuthenticated ? principal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Role)?.Value : string.Empty; + Claims = IsAuthenticated + ? principal.Claims + .GroupBy(c => c.Type) + .ToDictionary( + k => k.Key, + v => v.Select(c => c.Value)) + : new Dictionary>(); + } + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs new file mode 100644 index 0000000..0caba88 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -0,0 +1,30 @@ +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF; + +public class BreweryDbContext : DbContext +{ + + public DbSet Beers { get; set; } + public DbSet Brewers { get; set; } + public DbSet Breweries { get; set; } + public DbSet Wholesalers { get; set; } + public DbSet BeerSales { get; set; } + public DbSet BeerStocks { get; set; } + public DbSet BeerOrder { get; set; } + public DbSet BeerQuotes { get; set; } + public DbSet Users { get; set; } + + public BreweryDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + modelBuilder.HasDefaultSchema("brewery"); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs b/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs new file mode 100644 index 0000000..ac729db --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs @@ -0,0 +1,13 @@ +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Brewery.Infrastructure.EF.Configuration; + +public class BeerOrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(b => b.BeerId); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs b/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs new file mode 100644 index 0000000..1ccf01d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Brewery.Infrastructure.EF.Configuration; + +public class UserConfiguration : IEntityTypeConfiguration +{ + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + public void Configure(EntityTypeBuilder builder) + { + builder.Property(u => u.Claims) + .HasConversion(c => JsonSerializer.Serialize(c, _jsonOptions), + json => JsonSerializer.Deserialize>>(json, _jsonOptions)); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs new file mode 100644 index 0000000..7b46897 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -0,0 +1,33 @@ +using Brewery.Abstractions.Postgres; +using Brewery.Domain.Repositories; +using Brewery.Infrastructure.EF.Repositories; +using Brewery.Infrastructure.EF.UnitOfWork; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.EF; + +public static class Extensions +{ + internal static IServiceCollection AddEF(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + var postgresOptions = services.GetOptions("postgres"); + services.AddDbContext(options => + { + options.UseNpgsql(postgresOptions.ConnectionString); + }); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs new file mode 100644 index 0000000..08cf86d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs @@ -0,0 +1,197 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241115202730_EditBeerToRemoveUnitPrice")] + partial class EditBeerToRemoveUnitPrice + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs new file mode 100644 index 0000000..187cd50 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs @@ -0,0 +1,181 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBeerToRemoveUnitPrice : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "brewery"); + + migrationBuilder.CreateTable( + name: "Breweries", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Breweries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Wholesalers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Wholesalers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Brewers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BreweryId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Brewers", x => x.Id); + table.ForeignKey( + name: "FK_Brewers_Breweries_BreweryId", + column: x => x.BreweryId, + principalSchema: "brewery", + principalTable: "Breweries", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "BeerSales", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WholesalerId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerSales", x => x.Id); + table.ForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + column: x => x.WholesalerId, + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Beers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BrewerId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Beers", x => x.Id); + table.ForeignKey( + name: "FK_Beers_Brewers_BrewerId", + column: x => x.BrewerId, + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BeerStocks", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + BrewerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerStocks", x => x.Id); + table.ForeignKey( + name: "FK_BeerStocks_Brewers_BrewerId", + column: x => x.BrewerId, + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Beers_BrewerId", + schema: "brewery", + table: "Beers", + column: "BrewerId"); + + migrationBuilder.CreateIndex( + name: "IX_BeerSales_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId"); + + migrationBuilder.CreateIndex( + name: "IX_BeerStocks_BrewerId", + schema: "brewery", + table: "BeerStocks", + column: "BrewerId"); + + migrationBuilder.CreateIndex( + name: "IX_Brewers_BreweryId", + schema: "brewery", + table: "Brewers", + column: "BreweryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Beers", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerSales", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerStocks", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Wholesalers", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Brewers", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Breweries", + schema: "brewery"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs new file mode 100644 index 0000000..98b0e00 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs @@ -0,0 +1,253 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241116163101_AddBeerOrder,BeerQuote")] + partial class AddBeerOrderBeerQuote + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs new file mode 100644 index 0000000..dcd9c80 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs @@ -0,0 +1,118 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddBeerOrderBeerQuote : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales"); + + migrationBuilder.AlterColumn( + name: "WholesalerId", + schema: "brewery", + table: "BeerSales", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "BeerQuotes", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + DiscountInPercent = table.Column(type: "integer", nullable: false), + Total = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerQuotes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BeerOrder", + schema: "brewery", + columns: table => new + { + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric", nullable: false), + Total = table.Column(type: "numeric", nullable: false), + BeerQuoteId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerOrder", x => x.BeerId); + table.ForeignKey( + name: "FK_BeerOrder_BeerQuotes_BeerQuoteId", + column: x => x.BeerQuoteId, + principalSchema: "brewery", + principalTable: "BeerQuotes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_BeerOrder_BeerQuoteId", + schema: "brewery", + table: "BeerOrder", + column: "BeerQuoteId"); + + migrationBuilder.AddForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId", + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales"); + + migrationBuilder.DropTable( + name: "BeerOrder", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerQuotes", + schema: "brewery"); + + migrationBuilder.AlterColumn( + name: "WholesalerId", + schema: "brewery", + table: "BeerSales", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId", + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs new file mode 100644 index 0000000..4abd787 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs @@ -0,0 +1,256 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241116175232_EditBeerSaleToAddUnitPrice")] + partial class EditBeerSaleToAddUnitPrice + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs new file mode 100644 index 0000000..89f313e --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBeerSaleToAddUnitPrice : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UnitPrice", + schema: "brewery", + table: "BeerSales", + type: "numeric", + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UnitPrice", + schema: "brewery", + table: "BeerSales"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs new file mode 100644 index 0000000..8e39f13 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs @@ -0,0 +1,286 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241119092325_AddUser")] + partial class AddUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs new file mode 100644 index 0000000..873dd56 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + Password = table.Column(type: "text", nullable: false), + Role = table.Column(type: "text", nullable: false), + Claims = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users", + schema: "brewery"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs new file mode 100644 index 0000000..34aed80 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -0,0 +1,283 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + partial class BreweryDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs new file mode 100644 index 0000000..03f60e6 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs @@ -0,0 +1,30 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class BrowseBeersByBreweryHandler : IQueryHandler> +{ + private readonly DbSet _breweries; + + public BrowseBeersByBreweryHandler(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + } + public async Task> QueryAsync(BrowseBeersByBrewery query) + { + var brewery = await _breweries + .AsNoTracking() + .Include(b => b.Brewers) + .ThenInclude(b => b.Beers) + .SingleOrDefaultAsync(b => b.Id == query.BreweryId); + + return brewery is not null + ? brewery?.Brewers + .SelectMany(b => b.Beers) + .Select(b => b.AsDto()) + : Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs new file mode 100644 index 0000000..cb9cd9d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBeerHandler : IQueryHandler +{ + private readonly DbSet _beers; + + public GetBeerHandler(BreweryDbContext dbContext) + { + _beers = dbContext.Beers; + } + + public async Task QueryAsync(GetBeer query) + { + var beer = await _beers.AsNoTracking() + .SingleOrDefaultAsync(beer => beer.Id == query.Id); + + return beer is not null + ? beer.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs new file mode 100644 index 0000000..525f9a5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs @@ -0,0 +1,28 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBeerQuoteHandler : IQueryHandler +{ + private readonly DbSet _beerQuotes; + + public GetBeerQuoteHandler(BreweryDbContext dbContext) + { + _beerQuotes = dbContext.BeerQuotes; + } + public async Task QueryAsync(GetBeerQuote query) + { + var beerQuote = await _beerQuotes + .AsNoTracking() + .Include(b => b.BeerOrders) + .SingleOrDefaultAsync(b => b.Id == query.BeerQuoteId); + + return beerQuote is not null + ? beerQuote.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs new file mode 100644 index 0000000..cd79497 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBrewerHandler : IQueryHandler +{ + private readonly DbSet _brewers; + + public GetBrewerHandler(BreweryDbContext dbContext) + { + _brewers = dbContext.Brewers; + } + public async Task QueryAsync(GetBrewer query) + { + var brewer = await _brewers + .AsNoTracking() + .SingleOrDefaultAsync(b => b.Id == query.BrewerId); + + return brewer is not null + ? brewer.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs new file mode 100644 index 0000000..2902e7d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs @@ -0,0 +1,31 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBreweryHandler : IQueryHandler +{ + private readonly DbSet _breweries; + + public GetBreweryHandler(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + } + + public async Task QueryAsync(GetBrewery query) + { + var brewery = await _breweries + .AsNoTracking() + .SingleOrDefaultAsync(b => b.Id == query.Id); + + return brewery is not null + ? new BreweryDto + { + Id = brewery.Id, + Name = brewery.Name, + } + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs new file mode 100644 index 0000000..f4ddb5e --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs @@ -0,0 +1,33 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetUserHandler : IQueryHandler +{ + private readonly DbSet _users; + + public GetUserHandler(BreweryDbContext dbContext) + { + _users = dbContext.Users; + } + public async Task QueryAsync(GetUser query) + { + var user = await _users + .AsNoTracking() + .SingleOrDefaultAsync(u => u.Id == query.UserId); + + return user is not null + ? new AccountDto + { + UserId = user.Id, + Email = user.Email, + Role = user.Role, + Claims = user.Claims, + } + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs new file mode 100644 index 0000000..28e3b91 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetWholesalerHandler : IQueryHandler +{ + private readonly DbSet _wholesalers; + + public GetWholesalerHandler(BreweryDbContext dbContext) + { + _wholesalers = dbContext.Wholesalers; + } + public async Task QueryAsync(GetWholesaler query) + { + var wholesaler = await _wholesalers + .AsNoTracking() + .SingleOrDefaultAsync(w => w.Id == query.WholesalerId); + + return wholesaler is not null + ? wholesaler.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs new file mode 100644 index 0000000..2475a58 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs @@ -0,0 +1,26 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerQuoteRepository : IBeerQuoteRepository +{ + private readonly DbSet _beerQuotes; + private readonly BreweryDbContext _dbContext; + + public BeerQuoteRepository(BreweryDbContext dbContext) + { + _beerQuotes = dbContext.BeerQuotes; + _dbContext = dbContext; + } + + public async Task AddAsync(BeerQuote beerQuote) + { + await _beerQuotes.AddAsync(beerQuote); + await _dbContext.SaveChangesAsync(); + } + + public async Task GetBeerQuote(Guid id) + => await _beerQuotes.SingleOrDefaultAsync(beerQuote => beerQuote.Id == id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs new file mode 100644 index 0000000..9f46055 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerRepository : IBeerRepository +{ + private readonly DbSet _beers; + private readonly BreweryDbContext _dbContext; + + public BeerRepository(BreweryDbContext dbContext) + { + _beers = dbContext.Beers; + _dbContext = dbContext; + } + + public async Task AddAsync(Beer beer) + { + await _beers.AddAsync(beer); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(Beer beer) + { + _beers.Update(beer); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(Beer beer) + { + _beers.Remove(beer); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBeerById(Guid id) + => _beers.SingleOrDefaultAsync(beer => beer.Id == id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs new file mode 100644 index 0000000..b56a201 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerSaleRepository : IBeerSaleRepository +{ + private readonly DbSet _beerSales; + private readonly BreweryDbContext _dbContext; + + public BeerSaleRepository(BreweryDbContext dbContext) + { + _beerSales = dbContext.BeerSales; + _dbContext = dbContext; + } + + public async Task AddAsync(BeerSale beerSale) + { + await _beerSales.AddAsync(beerSale); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(BeerSale beerSale) + { + _beerSales.Update(beerSale); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(BeerSale beerSale) + { + _beerSales.Remove(beerSale); + await _dbContext.SaveChangesAsync(); + } + + public async Task> BrowseByBeerId(Guid beerId) + => await _beerSales.Where(b => b.BeerId == beerId).ToListAsync(); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs new file mode 100644 index 0000000..05e293c --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerStockRepository : IBeerStockRepository +{ + private readonly DbSet _beerStocks; + private readonly BreweryDbContext _dbContext; + + public BeerStockRepository(BreweryDbContext dbContext) + { + _beerStocks = dbContext.BeerStocks; + _dbContext = dbContext; + } + + public async Task AddBeerStock(BeerStock stock) + { + await _beerStocks.AddAsync(stock); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateBeerStock(BeerStock stock) + { + _beerStocks.Update(stock); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteBeerStock(BeerStock stock) + { + _beerStocks.Remove(stock); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBeerStock(Guid beerId) + => _beerStocks.SingleOrDefaultAsync(b => b.BeerId == beerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs new file mode 100644 index 0000000..d5620f5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BrewerRepository : IBrewerRepository +{ + private readonly DbSet _brewers; + private readonly BreweryDbContext _dbContext; + + public BrewerRepository(BreweryDbContext dbContext) + { + _brewers = dbContext.Brewers; + _dbContext = dbContext; + } + + public async Task AddBrewer(Brewer brewer) + { + await _brewers.AddAsync(brewer); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(Brewer brewer) + { + _brewers.Update(brewer); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(Brewer brewer) + { + _brewers.Remove(brewer); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBrewer(Guid brewerId) + => _brewers.SingleOrDefaultAsync(_brewers => _brewers.Id == brewerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs new file mode 100644 index 0000000..0d55682 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BreweryRepository : IBreweryRepository +{ + private readonly DbSet _breweries; + private readonly BreweryDbContext _dbContext; + + public BreweryRepository(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + _dbContext = dbContext; + } + + public async Task AddBrewery(Domain.Entities.Brewery brewery) + { + await _breweries.AddAsync(brewery); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateBrewery(Domain.Entities.Brewery brewery) + { + _breweries.Update(brewery); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteBrewery(Domain.Entities.Brewery brewery) + { + _breweries.Remove(brewery); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBreweryById(Guid breweryId) + => _breweries + .SingleOrDefaultAsync(b => b.Id == breweryId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs new file mode 100644 index 0000000..06c195d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs @@ -0,0 +1,29 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class UserRepository : IUserRepository +{ + private readonly DbSet _users; + private readonly BreweryDbContext _dbContext; + + public UserRepository(BreweryDbContext dbContext) + { + _users = dbContext.Users; + _dbContext = dbContext; + } + + public async Task AddAsync(User user) + { + await _users.AddAsync(user); + await _dbContext.SaveChangesAsync(); + } + + public Task GetUserById(Guid id) + => _users.SingleOrDefaultAsync(u => u.Id == id); + + public Task GetUserByEmail(string email) + => _users.SingleOrDefaultAsync(u => u.Email == email); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs new file mode 100644 index 0000000..181dd83 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs @@ -0,0 +1,32 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class WholesalerRepository : IWholesalerRepository +{ + private readonly DbSet _wholesalers; + private readonly BreweryDbContext _dbContext; + + public WholesalerRepository(BreweryDbContext dbContext) + { + _wholesalers = dbContext.Wholesalers; + _dbContext = dbContext; + } + + public async Task AddWholesaler(Wholesaler wholesaler) + { + await _wholesalers.AddAsync(wholesaler); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateWholesaler(Wholesaler wholesaler) + { + _wholesalers.Update(wholesaler); + await _dbContext.SaveChangesAsync(); + } + + public Task GetWholesaler(Guid id) + => _wholesalers.SingleOrDefaultAsync(w => w.Id == id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs new file mode 100644 index 0000000..20e68e1 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public class BreweryUnitOfWork : IUnitOfWork +{ + private readonly BreweryDbContext _dbContext; + + public BreweryUnitOfWork(BreweryDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task ExecuteAsync(Func action) + { + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + try + { + await action(); + await transaction.CommitAsync(); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs new file mode 100644 index 0000000..c2468c5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public interface IBreweryUnitOfWork where TDbContext : DbContext +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs new file mode 100644 index 0000000..6a64366 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public interface IUnitOfWork +{ + Task ExecuteAsync(Func action); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs b/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs new file mode 100644 index 0000000..9419f0e --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs @@ -0,0 +1,29 @@ +using System.Net; +using Brewery.Abstractions.Exceptions; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Exceptions; + +public class ExceptionMiddleware : IMiddleware +{ + private readonly IExceptionToResponseMapper _exceptionToResponseMapper; + + public ExceptionMiddleware(IExceptionToResponseMapper exceptionToResponseMapper) + { + _exceptionToResponseMapper = exceptionToResponseMapper; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception ex) + { + var response = _exceptionToResponseMapper.Map(ex); + context.Response.StatusCode = (int)response.HttpStatusCode; + await context.Response.WriteAsJsonAsync(response.Error); + } + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 0000000..a28e8bb --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,22 @@ +using System.Net; +using Brewery.Abstractions.Exceptions; +using Humanizer; + +namespace Brewery.Infrastructure.Exceptions; + +public class ExceptionToResponseMapper : IExceptionToResponseMapper +{ + public ExceptionResponse Map(Exception exception) + => exception switch + { + BreweryException breweryException => new ExceptionResponse( + new Error(GetErrorCode(exception), breweryException.Message), + HttpStatusCode.BadRequest), + _ => new ExceptionResponse( + new Error("error", "There was an error."), + HttpStatusCode.InternalServerError) + }; + + private static string GetErrorCode(Exception exception) + => exception.GetType().Name.Underscore().Replace("_exception", string.Empty); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/Extensions.cs b/src/Brewery.Infrastructure/Exceptions/Extensions.cs new file mode 100644 index 0000000..6441f82 --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/Extensions.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Exceptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Exceptions; + +public static class Extensions +{ + public static IServiceCollection AddExceptionHanding(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + + return services; + } + + public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs new file mode 100644 index 0000000..6fb00ff --- /dev/null +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using Brewery.Infrastructure.Auth; +using Brewery.Infrastructure.Commands; +using Brewery.Infrastructure.Contexts; +using Brewery.Infrastructure.EF; +using Brewery.Infrastructure.Exceptions; +using Brewery.Infrastructure.Messaging; +using Brewery.Infrastructure.Middleware; +using Brewery.Infrastructure.Queries; +using Brewery.Infrastructure.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure; + +public static class Extensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IList assemblies) + { + services.AddAuth(); + services.AddContexts(); + services.AddControllers(); + services.AddExceptionHanding(); + services.AddHostedService(); + services.AddSingleton(); + services.AddEF(); + services.AddCommands(assemblies); + services.AddQueries(assemblies); + services.AddRabbitMq(); + + return services; + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseExceptionHandling(); + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(e => + { + e.MapControllers(); + e.MapGet("/", () => "Hello from Brewery Api!"); + e.MapGet("/endpoints", async (HttpContext context, EndpointsInfoMiddleware middleware) => + { + await middleware.InvokeAsync(context, _ => Task.CompletedTask); + }); + }); + + return app; + } + + public static TOptions GetOptions(this IServiceCollection services, string sectionName) + where TOptions : class, new() + { + using var serviceProvider = services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + + return configuration.GetOptions(sectionName); + } + + public static TOptions GetOptions(this IConfiguration configuration, string sectionName) + where TOptions : class, new() + { + var options = new TOptions(); + var section = configuration.GetSection(sectionName); + section.Bind(options); + + return options; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/Extensions.cs b/src/Brewery.Infrastructure/Messaging/Extensions.cs new file mode 100644 index 0000000..aa1ba51 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/Extensions.cs @@ -0,0 +1,18 @@ +using Brewery.Abstractions.Messaging; +using Brewery.Infrastructure.Messaging.RabbitMq; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Messaging; + +public static class Extensions +{ + public static IServiceCollection AddRabbitMq(this IServiceCollection services) + { + var options = services.GetOptions("rabbitmq"); + services.AddSingleton(options); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs new file mode 100644 index 0000000..5140b1f --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -0,0 +1,104 @@ +using System.Text; +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Messaging; +using Brewery.Infrastructure.Messaging.RabbitMq; +using Humanizer; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Brewery.Infrastructure.Messaging; + +public class RabbitMessagePublisher : IMessagePublisher +{ + private readonly RabbitMqOptions _options; + private IConnectionManager _connectionManager; + private readonly ILogger _logger; + + public RabbitMessagePublisher(RabbitMqOptions options, + ILogger logger, + IConnectionManager connectionManager) + { + _options = options; + _connectionManager = connectionManager; + _logger = logger; + } + + public async Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage + { + var channel = await _connectionManager.CreateChannel(); + + var correlationId = Guid.NewGuid().ToString(); + var callbackQueue = "brewery_id_service_callback_queue"; + + var routingKey = message.GetType().Name.Underscore(); + var json = JsonConvert.SerializeObject(message); + var body = Encoding.UTF8.GetBytes(json); + + var properties = new BasicProperties(); + properties.CorrelationId = correlationId; + properties.ReplyTo = callbackQueue; + + await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + + await channel.BasicPublishAsync( + exchange, + routingKey, + false, + properties, + body); + } + + public async Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage where TResult : JsonWebToken + { + var channel = await _connectionManager.CreateChannel(); + + var correlationId = Guid.NewGuid().ToString(); + var callbackQueue = "brewery_id_service_callback_queue"; + + var routingKey = message.GetType().Name.Underscore(); + var json = JsonConvert.SerializeObject(message); + var body = Encoding.UTF8.GetBytes(json); + + var properties = new BasicProperties(); + properties.CorrelationId = correlationId; + properties.ReplyTo = callbackQueue; + + await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + + await channel.BasicPublishAsync( + exchange: exchange, + routingKey: routingKey, + mandatory: false, + basicProperties: properties, + body: body); + + var tcs = new TaskCompletionSource(); + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += async (model, ea) => + { + _logger.LogInformation("Start consuming on message received"); + if (ea.BasicProperties.CorrelationId == correlationId) + { + var responseJson = Encoding.UTF8.GetString(ea.Body.ToArray()); + var responseMessage = JsonConvert.DeserializeObject(responseJson); + + tcs.SetResult(responseMessage); + + await channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, false); + } + + }; + + await channel.BasicConsumeAsync(queue: callbackQueue, autoAck: false, consumer: consumer); + var result = await tcs.Task; + + await channel.DisposeAsync(); + + return result; + + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs new file mode 100644 index 0000000..b6f605b --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs @@ -0,0 +1,26 @@ +using Brewery.Abstractions.Messaging; +using RabbitMQ.Client; + +namespace Brewery.Infrastructure.Messaging.RabbitMq; + +public class ConnectionManager : IConnectionManager +{ + private readonly IConnectionFactory _factory; + private readonly RabbitMqOptions _options; + private readonly Lazy> _lazyConnection; + + public ConnectionManager(RabbitMqOptions options) + { + _options = options; + _factory = new ConnectionFactory { HostName = _options.HostName }; + _lazyConnection = new Lazy>(() => _factory.CreateConnectionAsync()); + } + + public async Task CreateChannel() + { + var connection = await _lazyConnection.Value; + var channel = await connection.CreateChannelAsync(); + + return channel; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs new file mode 100644 index 0000000..fb45807 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs @@ -0,0 +1,8 @@ +using RabbitMQ.Client; + +namespace Brewery.Infrastructure.Messaging.RabbitMq; + +public interface IConnectionManager +{ + Task CreateChannel(); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs b/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs new file mode 100644 index 0000000..151022c --- /dev/null +++ b/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Brewery.Infrastructure.Middleware; + +public class EndpointsInfoMiddleware : IMiddleware +{ + private readonly EndpointDataSource _endpointDataSource; + + public EndpointsInfoMiddleware(EndpointDataSource endpointDataSource) + { + _endpointDataSource = endpointDataSource; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var endpoints = _endpointDataSource.Endpoints + .Select(e => + { + var routeEndpoint = e as RouteEndpoint; + + return new + { + Endpoint = e.DisplayName, + Methods = string.Join(", ", e.Metadata.OfType() + .Select(m => string.Join(", ", m.HttpMethods))), + Route = routeEndpoint?.RoutePattern?.RawText, + }; + }) + .ToArray(); + + await context.Response.WriteAsJsonAsync(endpoints); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Queries/Extensions.cs b/src/Brewery.Infrastructure/Queries/Extensions.cs new file mode 100644 index 0000000..cfe2959 --- /dev/null +++ b/src/Brewery.Infrastructure/Queries/Extensions.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using Brewery.Abstractions.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Queries; + +public static class Extensions +{ + public static IServiceCollection AddQueries(this IServiceCollection services, IList assemblies) + { + services.AddSingleton(); + services.Scan(a => a.FromAssemblies(assemblies) + .AddClasses(c => c.AssignableTo(typeof(IQueryHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime()); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs new file mode 100644 index 0000000..f1e6bde --- /dev/null +++ b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Queries; + +public class QueryDispatcher : IQueryDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public QueryDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task QueryAsync(IQuery query) + { + using var scope = _serviceProvider.CreateScope(); + var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); + var handler = scope.ServiceProvider.GetRequiredService(handlerType); + + return await (Task)handlerType.GetMethod(nameof(IQueryHandler,TResult>.QueryAsync)) + ?.Invoke(handler, new[] { query }); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Services/AppInitializer.cs b/src/Brewery.Infrastructure/Services/AppInitializer.cs new file mode 100644 index 0000000..39459fa --- /dev/null +++ b/src/Brewery.Infrastructure/Services/AppInitializer.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Brewery.Infrastructure.Services; + +public class AppInitializer : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly IHostEnvironment _hostEnvironment; + + public AppInitializer(IServiceProvider serviceProvider, + IHostEnvironment hostEnvironment) + { + _serviceProvider = serviceProvider; + _hostEnvironment = hostEnvironment; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (_hostEnvironment.IsEnvironment("test")) + { + return; + } + + var dbContextTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(t => t.GetTypes()) + .Where(t => typeof(DbContext).IsAssignableFrom(t) && !t.IsInterface && t != typeof(DbContext)); + + using var scope = _serviceProvider.CreateScope(); + foreach (var dbType in dbContextTypes) + { + var dbContext = scope.ServiceProvider.GetService(dbType) as DbContext; + await dbContext.Database.MigrateAsync(); + } + } + + private async Task SeedDatabase() + { + + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs b/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs new file mode 100644 index 0000000..e4b7a85 --- /dev/null +++ b/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Hosting; + +namespace Brewery.Infrastructure.Services; + +public class BackgroundConsumer : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public BackgroundConsumer(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj b/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj new file mode 100644 index 0000000..e551d75 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs b/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs new file mode 100644 index 0000000..8998688 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Text; +using Brewery.Application.Commands; +using Brewery.Domain.Entities; +using Brewery.Tests.Shared.Factories; +using Brewery.Tests.Shared.Fixtures; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Shouldly; + +namespace Brewery.Tests.EndToEnd.Sync; + +[Collection($"BreweryDbFixture")] +public class AddBeerTests : IClassFixture, IClassFixture +{ + private Task Act() => _httpClient.PostAsync("Beer", GetPayload(_command)); + + [Fact] + public async Task add_beer_endpoint_should_return_status_code_created_at_action() + { + await SeedDatabase(); + + var response = await Act(); + + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.Created); + } + + [Fact] + public async Task add_beer_endpoint_should_return_location_header_with_correct_beer_id() + { + await SeedDatabase(); + + var response = await Act(); + + var locationHeader = response.Headers + .FirstOrDefault(h => h.Key == "Location") + .Value.First(); + locationHeader.ShouldNotBeNull(); + locationHeader.ShouldBe($"http://localhost/Beer/{_command.Id}"); + } + + [Fact] + public async Task add_beer_endpoint_should_add_beer_entity_with_given_id_to_database() + { + await SeedDatabase(); + + var response = await Act(); + + var beerFromDb = await _breweryDbFixture.BreweryDbContext.Beers.SingleOrDefaultAsync(b => b.Id == _beerId); + beerFromDb.ShouldNotBeNull(); + beerFromDb.Id.ShouldBe(_command.Id); + } + + private readonly HttpClient _httpClient; + private readonly BreweryDbFixture _breweryDbFixture; + private readonly Guid _beerId; + private readonly Guid _brewerId; + private readonly AddBeer _command; + + public AddBeerTests(BreweryAppFactory factory, BreweryDbFixture breweryDbFixture) + { + factory.Server.AllowSynchronousIO = true; + _httpClient = factory.CreateClient(); + _breweryDbFixture = breweryDbFixture; + _beerId = Guid.NewGuid(); + _brewerId = Guid.NewGuid(); + _command = new AddBeer(_brewerId, "beer 1") { Id = _beerId }; + } + + private StringContent GetPayload(AddBeer command) + { + var json = JsonConvert.SerializeObject(command); + return new StringContent(json, Encoding.UTF8, "application/json"); + } + + private async Task SeedDatabase() + { + var brewer = Brewer.Create(_brewerId, "brewer 1"); + await _breweryDbFixture.BreweryDbContext.Brewers.AddAsync(brewer); + await _breweryDbFixture.BreweryDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs b/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs new file mode 100644 index 0000000..2bcb714 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs @@ -0,0 +1,44 @@ +using System.Net; +using System.Text; +using Brewery.Application.Commands; +using Brewery.Tests.Shared.Factories; +using Brewery.Tests.Shared.Fixtures; +using Newtonsoft.Json; +using Shouldly; + +namespace Brewery.Tests.EndToEnd.Sync; + +[Collection($"BreweryDbFixture")] +public class AddBrewerTests : IClassFixture, IClassFixture +{ + private async Task Act() => await _client.PostAsync("brewer", GetPayload()); + + [Fact] + public async Task add_brewer_endpoint_should_return_status_code_created_at_action() + { + await _breweryDbFixture.SeedDatabaseAsync(_breweryId); + + var response = await Act(); + + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.Created); + } + + private readonly HttpClient _client; + private readonly BreweryDbFixture _breweryDbFixture; + private readonly Guid _breweryId; + + public AddBrewerTests(BreweryAppFactory factory, BreweryDbFixture breweryDbFixture) + { + factory.Server.AllowSynchronousIO = true; + _client = factory.CreateClient(); + _breweryDbFixture = breweryDbFixture; + _breweryId = Guid.NewGuid(); + } + + private StringContent GetPayload() + { + var json = JsonConvert.SerializeObject(new AddBrewer("brewer 1", _breweryId)); + return new StringContent(json, Encoding.UTF8, "application/json"); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj b/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj new file mode 100644 index 0000000..8d26f67 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs b/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs new file mode 100644 index 0000000..759ace3 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace Brewery.Tests.Shared.Factories; + +public class BreweryAppFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("test"); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs b/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs new file mode 100644 index 0000000..95b2915 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs @@ -0,0 +1,43 @@ +using Brewery.Abstractions.Postgres; +using Brewery.Infrastructure.EF; +using Brewery.Tests.Shared.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Tests.Shared.Fixtures; + +public class BreweryDbFixture : IAsyncLifetime +{ + public BreweryDbContext BreweryDbContext { get; private set; } + + public BreweryDbFixture() + { + + } + + public Task InitializeAsync() + { + var postgresOptions = OptionsHelper.GetOptions("postgres"); + var dbContextOptions = new DbContextOptionsBuilder() + .UseNpgsql(postgresOptions.ConnectionString) + .Options; + BreweryDbContext = new BreweryDbContext(dbContextOptions); + + BreweryDbContext.Database.EnsureDeleted(); + BreweryDbContext.Database.EnsureCreated(); + + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + await BreweryDbContext.DisposeAsync(); + } + + public async Task SeedDatabaseAsync(Guid breweryId) + { + var brewery = Domain.Entities.Brewery.Create(breweryId, "Brewery 1"); + + await BreweryDbContext.AddAsync(brewery); + await BreweryDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs b/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs new file mode 100644 index 0000000..d960ec0 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; + +namespace Brewery.Tests.Shared.Helpers; + +public class OptionsHelper +{ + private const string AppSettings = "appsettings.test.json"; + public static TOptions GetOptions(string sectionName) where TOptions : new() + { + TOptions options = new TOptions(); + var configuration = GetConfiguration(); + var section = configuration.GetSection(sectionName); + section.Bind(options); + + return options; + } + + private static IConfigurationRoot GetConfiguration() + => new ConfigurationBuilder() + .AddJsonFile(AppSettings) + .AddEnvironmentVariables() + .Build(); +} \ No newline at end of file