From bf3f89b631eba0ca3ff97ec21454d86f47dbaa3b Mon Sep 17 00:00:00 2001
From: RieBi <42877671+RieBi@users.noreply.github.com>
Date: Sun, 28 Jul 2024 18:42:04 +0200
Subject: [PATCH] Add whole project
---
BrewABear/.dockerignore | 30 +++
BrewABear/.github/workflows/deploy.yml | 42 ++++
BrewABear/.github/workflows/dotnet.yml | 28 +++
BrewABear/Api/Api.csproj | 22 +++
BrewABear/Api/Api.http | 6 +
BrewABear/Api/Controllers/BrewerController.cs | 108 ++++++++++
.../Api/Controllers/BreweryController.cs | 78 ++++++++
BrewABear/Api/Controllers/OrderController.cs | 81 ++++++++
BrewABear/Api/Controllers/SaleController.cs | 33 ++++
BrewABear/Api/Controllers/TeaController.cs | 35 ++++
.../Api/Controllers/WholesalerController.cs | 61 ++++++
BrewABear/Api/Dockerfile | 28 +++
BrewABear/Api/Program.cs | 58 ++++++
BrewABear/Api/Properties/launchSettings.json | 52 +++++
.../Api/Services/ExceptionHandlerService.cs | 81 ++++++++
.../Api/Services/IExceptionHandlerService.cs | 7 +
BrewABear/Api/appsettings.Development.json | 8 +
BrewABear/Api/appsettings.json | 12 ++
BrewABear/Application/Application.csproj | 27 +++
.../BrewerCommands/CreateBeerCommand.cs | 2 +
.../CreateBeerCommandHandler.cs | 29 +++
.../BrewerCommands/DeleteBeerCommand.cs | 2 +
.../DeleteBeerCommandHandler.cs | 22 +++
.../BrewerCommands/UpdateBeerCommand.cs | 2 +
.../UpdateBeerCommandHandler.cs | 29 +++
.../OrderCommands/CreateOrderCommand.cs | 2 +
.../CreateOrderCommandHandler.cs | 41 ++++
.../OrderCommands/RequestQuoteCommand.cs | 2 +
.../RequestQuoteCommandHandler.cs | 30 +++
.../SaleCommands/CreateSaleCommand.cs | 2 +
.../SaleCommands/CreateSaleCommandHandler.cs | 41 ++++
BrewABear/Application/DTOs/BeerCreateDto.cs | 8 +
BrewABear/Application/DTOs/BeerDto.cs | 10 +
BrewABear/Application/DTOs/BrewerDto.cs | 9 +
BrewABear/Application/DTOs/BreweryDto.cs | 7 +
BrewABear/Application/DTOs/OrderCreateDto.cs | 8 +
BrewABear/Application/DTOs/OrderDto.cs | 12 ++
BrewABear/Application/DTOs/QuoteInfoDto.cs | 8 +
BrewABear/Application/DTOs/WholesalerDto.cs | 6 +
.../DTOs/WholesalerInventoryDto.cs | 7 +
.../Exceptions/BeerNotFoundException.cs | 2 +
.../Exceptions/BrewABeerException.cs | 2 +
.../Exceptions/BrewerNotFoundException.cs | 2 +
.../Exceptions/BreweryNotFoundException.cs | 2 +
BrewABear/Application/Exceptions/ErrorDto.cs | 6 +
.../Exceptions/IAmTeapotException.cs | 2 +
.../Exceptions/NegativePriceException.cs | 5 +
.../Exceptions/NegativeQuantityException.cs | 5 +
.../Exceptions/OrderNotFoundException.cs | 2 +
.../Exceptions/ResourceNotFoundException.cs | 5 +
.../Exceptions/WholesalerNotFoundException.cs | 2 +
BrewABear/Application/IStarter.cs | 2 +
BrewABear/Application/MappingProfiles.cs | 19 ++
.../BrewerQueries/GetAllBeersByBrewerQuery.cs | 2 +
.../GetAllBeersByBrewerQueryHandler.cs | 28 +++
.../BrewerQueries/GetBrewerDetailsQuery.cs | 2 +
.../GetBrewerDetailsQueryHandler.cs | 21 ++
.../GetAllBeersInBreweryQuery.cs | 2 +
.../GetAllBeersInBreweryQueryHandler.cs | 29 +++
.../BreweryQueries/GetAllBreweriesQuery.cs | 2 +
.../GetAllBreweriesQueryHandler.cs | 15 ++
.../GetAllBrewersInBreweryQuery.cs | 2 +
.../GetAllBrewersInBreweryQueryHandler.cs | 27 +++
.../BreweryQueries/GetBreweryDetailsQuery.cs | 2 +
.../GetBreweryDetailsQueryHandler.cs | 21 ++
.../Queries/OrderQueries/GetAllOrdersQuery.cs | 2 +
.../OrderQueries/GetAllOrdersQueryHandler.cs | 23 +++
.../OrderQueries/GetOrderDetailsQuery.cs | 2 +
.../GetOrderDetailsQueryHandler.cs | 25 +++
.../GetAllWholesalersQuery.cs | 2 +
.../GetAllWholesalersQueryHandler.cs | 18 ++
.../GetWholesalerDetailsQuery.cs | 2 +
.../GetWholesalerDetailsQueryHandler.cs | 21 ++
.../GetWholesalerInventoryQuery.cs | 2 +
.../GetWholesalerInventoryQueryHandler.cs | 27 +++
BrewABear/Application/Services/GuidCreator.cs | 5 +
.../Application/Services/IGuidCreator.cs | 6 +
.../Application/Services/IOrderService.cs | 9 +
.../Application/Services/ISaleService.cs | 7 +
.../Application/Services/OrderService.cs | 16 ++
BrewABear/Application/Services/SaleService.cs | 29 +++
BrewABear/BrewABear.sln | 57 ++++++
BrewABear/Data/Data.csproj | 19 ++
BrewABear/Data/DataContext.cs | 22 +++
BrewABear/Data/DataSeeding/DataSeeder.cs | 186 ++++++++++++++++++
BrewABear/Data/DependencyInjection.cs | 17 ++
.../ModelConfigurations/BeerConfiguration.cs | 19 ++
.../BeerSaleConfiguration.cs | 15 ++
.../BrewerConfiguration.cs | 25 +++
.../BreweryConfiguration.cs | 19 ++
.../ModelConfigurations/OrderConfiguration.cs | 18 ++
.../WholesalerConfiguration.cs | 19 ++
.../WholesalerInventoryConfiguration.cs | 18 ++
BrewABear/Domain/Domain.csproj | 9 +
BrewABear/Domain/DomainConfig.cs | 6 +
BrewABear/Domain/Models/Beer.cs | 11 ++
BrewABear/Domain/Models/BeerSale.cs | 8 +
BrewABear/Domain/Models/Brewer.cs | 11 ++
BrewABear/Domain/Models/Brewery.cs | 9 +
BrewABear/Domain/Models/Order.cs | 13 ++
BrewABear/Domain/Models/Wholesaler.cs | 8 +
.../Domain/Models/WholesalerInventory.cs | 9 +
.../IntegrationTests/ApiApplicationFactory.cs | 48 +++++
BrewABear/IntegrationTests/BasicTests.cs | 37 ++++
.../ControllerTests/BrewerControllerTests.cs | 97 +++++++++
.../ControllerTests/BreweryControllerTests.cs | 73 +++++++
.../ControllerTests/OrderControllerTests.cs | 105 ++++++++++
.../ControllerTests/SaleControllerTests.cs | 35 ++++
.../WholesalerControllerTests.cs | 33 ++++
.../IntegrationTests/IntegrationTests.csproj | 35 ++++
BrewABear/LICENSE | 21 ++
BrewABear/README.md | 165 ++++++++++++++++
.../Application/Services/GuidCreatorTests.cs | 27 +++
.../Application/Services/OrderServiceTests.cs | 47 +++++
.../Application/Services/SaleServiceTests.cs | 134 +++++++++++++
BrewABear/Tests/Tests.csproj | 28 +++
116 files changed, 2884 insertions(+)
create mode 100644 BrewABear/.dockerignore
create mode 100644 BrewABear/.github/workflows/deploy.yml
create mode 100644 BrewABear/.github/workflows/dotnet.yml
create mode 100644 BrewABear/Api/Api.csproj
create mode 100644 BrewABear/Api/Api.http
create mode 100644 BrewABear/Api/Controllers/BrewerController.cs
create mode 100644 BrewABear/Api/Controllers/BreweryController.cs
create mode 100644 BrewABear/Api/Controllers/OrderController.cs
create mode 100644 BrewABear/Api/Controllers/SaleController.cs
create mode 100644 BrewABear/Api/Controllers/TeaController.cs
create mode 100644 BrewABear/Api/Controllers/WholesalerController.cs
create mode 100644 BrewABear/Api/Dockerfile
create mode 100644 BrewABear/Api/Program.cs
create mode 100644 BrewABear/Api/Properties/launchSettings.json
create mode 100644 BrewABear/Api/Services/ExceptionHandlerService.cs
create mode 100644 BrewABear/Api/Services/IExceptionHandlerService.cs
create mode 100644 BrewABear/Api/appsettings.Development.json
create mode 100644 BrewABear/Api/appsettings.json
create mode 100644 BrewABear/Application/Application.csproj
create mode 100644 BrewABear/Application/Commands/BrewerCommands/CreateBeerCommand.cs
create mode 100644 BrewABear/Application/Commands/BrewerCommands/CreateBeerCommandHandler.cs
create mode 100644 BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommand.cs
create mode 100644 BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommandHandler.cs
create mode 100644 BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommand.cs
create mode 100644 BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommandHandler.cs
create mode 100644 BrewABear/Application/Commands/OrderCommands/CreateOrderCommand.cs
create mode 100644 BrewABear/Application/Commands/OrderCommands/CreateOrderCommandHandler.cs
create mode 100644 BrewABear/Application/Commands/OrderCommands/RequestQuoteCommand.cs
create mode 100644 BrewABear/Application/Commands/OrderCommands/RequestQuoteCommandHandler.cs
create mode 100644 BrewABear/Application/Commands/SaleCommands/CreateSaleCommand.cs
create mode 100644 BrewABear/Application/Commands/SaleCommands/CreateSaleCommandHandler.cs
create mode 100644 BrewABear/Application/DTOs/BeerCreateDto.cs
create mode 100644 BrewABear/Application/DTOs/BeerDto.cs
create mode 100644 BrewABear/Application/DTOs/BrewerDto.cs
create mode 100644 BrewABear/Application/DTOs/BreweryDto.cs
create mode 100644 BrewABear/Application/DTOs/OrderCreateDto.cs
create mode 100644 BrewABear/Application/DTOs/OrderDto.cs
create mode 100644 BrewABear/Application/DTOs/QuoteInfoDto.cs
create mode 100644 BrewABear/Application/DTOs/WholesalerDto.cs
create mode 100644 BrewABear/Application/DTOs/WholesalerInventoryDto.cs
create mode 100644 BrewABear/Application/Exceptions/BeerNotFoundException.cs
create mode 100644 BrewABear/Application/Exceptions/BrewABeerException.cs
create mode 100644 BrewABear/Application/Exceptions/BrewerNotFoundException.cs
create mode 100644 BrewABear/Application/Exceptions/BreweryNotFoundException.cs
create mode 100644 BrewABear/Application/Exceptions/ErrorDto.cs
create mode 100644 BrewABear/Application/Exceptions/IAmTeapotException.cs
create mode 100644 BrewABear/Application/Exceptions/NegativePriceException.cs
create mode 100644 BrewABear/Application/Exceptions/NegativeQuantityException.cs
create mode 100644 BrewABear/Application/Exceptions/OrderNotFoundException.cs
create mode 100644 BrewABear/Application/Exceptions/ResourceNotFoundException.cs
create mode 100644 BrewABear/Application/Exceptions/WholesalerNotFoundException.cs
create mode 100644 BrewABear/Application/IStarter.cs
create mode 100644 BrewABear/Application/MappingProfiles.cs
create mode 100644 BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQuery.cs
create mode 100644 BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQuery.cs
create mode 100644 BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQuery.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQuery.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQuery.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQuery.cs
create mode 100644 BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/OrderQueries/GetAllOrdersQuery.cs
create mode 100644 BrewABear/Application/Queries/OrderQueries/GetAllOrdersQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQuery.cs
create mode 100644 BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQuery.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQuery.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQueryHandler.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQuery.cs
create mode 100644 BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQueryHandler.cs
create mode 100644 BrewABear/Application/Services/GuidCreator.cs
create mode 100644 BrewABear/Application/Services/IGuidCreator.cs
create mode 100644 BrewABear/Application/Services/IOrderService.cs
create mode 100644 BrewABear/Application/Services/ISaleService.cs
create mode 100644 BrewABear/Application/Services/OrderService.cs
create mode 100644 BrewABear/Application/Services/SaleService.cs
create mode 100644 BrewABear/BrewABear.sln
create mode 100644 BrewABear/Data/Data.csproj
create mode 100644 BrewABear/Data/DataContext.cs
create mode 100644 BrewABear/Data/DataSeeding/DataSeeder.cs
create mode 100644 BrewABear/Data/DependencyInjection.cs
create mode 100644 BrewABear/Data/ModelConfigurations/BeerConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/BeerSaleConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/BrewerConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/BreweryConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/OrderConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/WholesalerConfiguration.cs
create mode 100644 BrewABear/Data/ModelConfigurations/WholesalerInventoryConfiguration.cs
create mode 100644 BrewABear/Domain/Domain.csproj
create mode 100644 BrewABear/Domain/DomainConfig.cs
create mode 100644 BrewABear/Domain/Models/Beer.cs
create mode 100644 BrewABear/Domain/Models/BeerSale.cs
create mode 100644 BrewABear/Domain/Models/Brewer.cs
create mode 100644 BrewABear/Domain/Models/Brewery.cs
create mode 100644 BrewABear/Domain/Models/Order.cs
create mode 100644 BrewABear/Domain/Models/Wholesaler.cs
create mode 100644 BrewABear/Domain/Models/WholesalerInventory.cs
create mode 100644 BrewABear/IntegrationTests/ApiApplicationFactory.cs
create mode 100644 BrewABear/IntegrationTests/BasicTests.cs
create mode 100644 BrewABear/IntegrationTests/ControllerTests/BrewerControllerTests.cs
create mode 100644 BrewABear/IntegrationTests/ControllerTests/BreweryControllerTests.cs
create mode 100644 BrewABear/IntegrationTests/ControllerTests/OrderControllerTests.cs
create mode 100644 BrewABear/IntegrationTests/ControllerTests/SaleControllerTests.cs
create mode 100644 BrewABear/IntegrationTests/ControllerTests/WholesalerControllerTests.cs
create mode 100644 BrewABear/IntegrationTests/IntegrationTests.csproj
create mode 100644 BrewABear/LICENSE
create mode 100644 BrewABear/README.md
create mode 100644 BrewABear/Tests/Application/Services/GuidCreatorTests.cs
create mode 100644 BrewABear/Tests/Application/Services/OrderServiceTests.cs
create mode 100644 BrewABear/Tests/Application/Services/SaleServiceTests.cs
create mode 100644 BrewABear/Tests/Tests.csproj
diff --git a/BrewABear/.dockerignore b/BrewABear/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/BrewABear/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/BrewABear/.github/workflows/deploy.yml b/BrewABear/.github/workflows/deploy.yml
new file mode 100644
index 0000000..6c180e8
--- /dev/null
+++ b/BrewABear/.github/workflows/deploy.yml
@@ -0,0 +1,42 @@
+name: Deploy app
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ -
+ name: Checkout
+ uses: actions/checkout@v3
+
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ -
+ name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASS }}
+
+ -
+ name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ push: true
+ tags: ${{ secrets.DOCKER_USERNAME }}/brewabear:latest
+ file: Api/Dockerfile
+ -
+ name: Publish image to webapp
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: 'brewabear'
+ publish-profile: ${{ secrets.PublishProfile }}
+ images: 'docker.io/riebisv/brewabear:latest'
diff --git a/BrewABear/.github/workflows/dotnet.yml b/BrewABear/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..217f7cb
--- /dev/null
+++ b/BrewABear/.github/workflows/dotnet.yml
@@ -0,0 +1,28 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Test
+ run: dotnet test --no-build --verbosity normal
diff --git a/BrewABear/Api/Api.csproj b/BrewABear/Api/Api.csproj
new file mode 100644
index 0000000..e21415c
--- /dev/null
+++ b/BrewABear/Api/Api.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ 1bebeb90-e52a-4370-a1c0-b9e82fc3e99d
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BrewABear/Api/Api.http b/BrewABear/Api/Api.http
new file mode 100644
index 0000000..d2711dd
--- /dev/null
+++ b/BrewABear/Api/Api.http
@@ -0,0 +1,6 @@
+@Api_HostAddress = http://localhost:5142
+
+GET {{Api_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/BrewABear/Api/Controllers/BrewerController.cs b/BrewABear/Api/Controllers/BrewerController.cs
new file mode 100644
index 0000000..1cae35e
--- /dev/null
+++ b/BrewABear/Api/Controllers/BrewerController.cs
@@ -0,0 +1,108 @@
+using Api.Services;
+using Application.Commands.BrewerCommands;
+using Application.DTOs;
+using Application.Exceptions;
+using Application.Queries.BrewerQueries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class BrewerController(IMediator mediator, IExceptionHandlerService exceptionHandler) : ControllerBase
+{
+ private readonly IMediator _mediator = mediator;
+ private readonly IExceptionHandlerService _exceptionHandler = exceptionHandler;
+
+ [HttpGet]
+ [Route("{id}/Beers")]
+ [ProducesResponseType>(200)]
+ [ProducesResponseType(404)]
+ public async Task>> Beers(string id)
+ {
+ try
+ {
+ var beers = await _mediator.Send(new GetAllBeersByBrewerQuery(id));
+
+ return Ok(beers);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpGet]
+ [Route("{id}/Details")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task> Details(string id)
+ {
+ try
+ {
+ var brewer = await _mediator.Send(new GetBrewerDetailsQuery(id));
+
+ return Ok(brewer);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpPost]
+ [Route("{id}/AddBeer")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(404)]
+ public async Task> AddBeer(string id, BeerCreateDto beerCreateDto)
+ {
+ try
+ {
+ var newBeer = await _mediator.Send(new CreateBeerCommand(id, beerCreateDto));
+
+ return Ok(newBeer);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpPut]
+ [Route("{id}/UpdateBeer")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(404)]
+ public async Task> UpdateBeer(string id, string beerId, BeerCreateDto beerCreateDto)
+ {
+ try
+ {
+ var updatedBeer = await _mediator.Send(new UpdateBeerCommand(id, beerId, beerCreateDto));
+
+ return Ok(updatedBeer);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpDelete]
+ [Route("{id}/DeleteBeer")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task DeleteBeer(string id, string beerId)
+ {
+ try
+ {
+ await _mediator.Send(new DeleteBeerCommand(id, beerId));
+
+ return Ok();
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/BrewABear/Api/Controllers/BreweryController.cs b/BrewABear/Api/Controllers/BreweryController.cs
new file mode 100644
index 0000000..4d369ea
--- /dev/null
+++ b/BrewABear/Api/Controllers/BreweryController.cs
@@ -0,0 +1,78 @@
+using Api.Services;
+using Application.DTOs;
+using Application.Exceptions;
+using Application.Queries.BreweryQueries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class BreweryController(IMediator mediator, IExceptionHandlerService exceptionHandler) : ControllerBase
+{
+ private readonly IMediator _mediator = mediator;
+ private readonly IExceptionHandlerService _exceptionHandler = exceptionHandler;
+
+ [HttpGet]
+ [Route("All")]
+ public async Task> All()
+ {
+ var breweries = await _mediator.Send(new GetAllBreweriesQuery());
+
+ return breweries;
+ }
+
+ [HttpGet]
+ [Route("{id}/Details")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task> Details(string id)
+ {
+ try
+ {
+ var brewery = await _mediator.Send(new GetBreweryDetailsQuery(id));
+
+ return Ok(brewery);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpGet]
+ [Route("{id}/Beers")]
+ [ProducesResponseType>(200)]
+ [ProducesResponseType(404)]
+ public async Task>> Beers(string id)
+ {
+ try
+ {
+ var beers = await _mediator.Send(new GetAllBeersInBreweryQuery(id));
+
+ return Ok(beers);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpGet]
+ [Route("{id}/Brewers")]
+ [ProducesResponseType>(200)]
+ [ProducesResponseType(404)]
+ public async Task>> Brewers(string id)
+ {
+ try
+ {
+ var brewers = await _mediator.Send(new GetAllBrewersInBreweryQuery(id));
+
+ return Ok(brewers);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/BrewABear/Api/Controllers/OrderController.cs b/BrewABear/Api/Controllers/OrderController.cs
new file mode 100644
index 0000000..9a7f7e4
--- /dev/null
+++ b/BrewABear/Api/Controllers/OrderController.cs
@@ -0,0 +1,81 @@
+using Api.Services;
+using Application.Commands.OrderCommands;
+using Application.DTOs;
+using Application.Exceptions;
+using Application.Queries.OrderQueries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class OrderController(IMediator mediator, IExceptionHandlerService exceptionHandler) : ControllerBase
+{
+ private readonly IMediator _mediator = mediator;
+ private readonly IExceptionHandlerService _exceptionHandler = exceptionHandler;
+
+ [HttpGet]
+ [Route("All")]
+ [ProducesResponseType>(200)]
+ public async Task>> All()
+ {
+ var orders = await _mediator.Send(new GetAllOrdersQuery());
+
+ return Ok(orders);
+ }
+
+ [HttpGet]
+ [Route("{id}/Details")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task> Details(string id)
+ {
+ try
+ {
+ var order = await _mediator.Send(new GetOrderDetailsQuery(id));
+
+ return Ok(order);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpPost]
+ [Route("Add")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(404)]
+ public async Task> Add(OrderCreateDto orderCreateDto)
+ {
+ try
+ {
+ var orderDto = await _mediator.Send(new CreateOrderCommand(orderCreateDto));
+
+ return Ok(orderDto);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpPost]
+ [Route("RequestQuote")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task> RequestQuote(string orderId)
+ {
+ try
+ {
+ var info = await _mediator.Send(new RequestQuoteCommand(orderId));
+
+ return Ok(info);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/BrewABear/Api/Controllers/SaleController.cs b/BrewABear/Api/Controllers/SaleController.cs
new file mode 100644
index 0000000..3574e4d
--- /dev/null
+++ b/BrewABear/Api/Controllers/SaleController.cs
@@ -0,0 +1,33 @@
+using Api.Services;
+using Application.Commands.SaleCommands;
+using Application.Exceptions;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class SaleController(IMediator mediator, IExceptionHandlerService exceptionHandler) : ControllerBase
+{
+ private readonly IMediator _mediator = mediator;
+ private readonly IExceptionHandlerService _exceptionHandler = exceptionHandler;
+
+ [HttpPost]
+ [Route("Add")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(404)]
+ public async Task Add(string wholesalerId, string beerId, int quantity)
+ {
+ try
+ {
+ await _mediator.Send(new CreateSaleCommand(wholesalerId, beerId, quantity));
+
+ return Ok();
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/BrewABear/Api/Controllers/TeaController.cs b/BrewABear/Api/Controllers/TeaController.cs
new file mode 100644
index 0000000..0960804
--- /dev/null
+++ b/BrewABear/Api/Controllers/TeaController.cs
@@ -0,0 +1,35 @@
+using Application.Exceptions;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class TeaController : ControllerBase
+{
+ [HttpGet]
+ [Route("GetTea")]
+ [ProducesResponseType(200)]
+ public string GetTea() => teaArt;
+
+ [HttpGet]
+ [Route("GetCoffee")]
+ [ProducesResponseType(418)]
+ public ActionResult GetCoffee()
+ {
+ var error = new ErrorDto(new PermanentlyTeapotException(), "I'm a teapot, not a coffemaker, permanently now.");
+ return new ObjectResult(error)
+ {
+ StatusCode = 418
+ };
+ }
+
+ private static readonly string teaArt = """
+ ;,'
+ _o_ ;:;'
+ ,-.'---`.__ ;
+ ((j`=====',-'
+ `-\ /
+ `-=-'
+
+ """;
+}
diff --git a/BrewABear/Api/Controllers/WholesalerController.cs b/BrewABear/Api/Controllers/WholesalerController.cs
new file mode 100644
index 0000000..700ff8d
--- /dev/null
+++ b/BrewABear/Api/Controllers/WholesalerController.cs
@@ -0,0 +1,61 @@
+using Api.Services;
+using Application.DTOs;
+using Application.Exceptions;
+using Application.Queries.WholesalerQueries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+[Route("api/[controller]")]
+[ApiController]
+public class WholesalerController(IMediator mediator, IExceptionHandlerService exceptionHandler) : ControllerBase
+{
+ private readonly IMediator _mediator = mediator;
+ private readonly IExceptionHandlerService _exceptionHandler = exceptionHandler;
+
+ [HttpGet]
+ [Route("All")]
+ [ProducesResponseType>(200)]
+ public async Task>> All()
+ {
+ var wholesalers = await _mediator.Send(new GetAllWholesalersQuery());
+
+ return Ok(wholesalers);
+ }
+
+ [HttpGet]
+ [Route("{id}/Details")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public async Task> Details(string id)
+ {
+ try
+ {
+ var wholesaler = await _mediator.Send(new GetWholesalerDetailsQuery(id));
+
+ return Ok(wholesaler);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+
+ [HttpGet]
+ [Route("{id}/Inventory")]
+ [ProducesResponseType>(200)]
+ [ProducesResponseType(404)]
+ public async Task>> Inventory(string id)
+ {
+ try
+ {
+ var wholesalerInventory = await _mediator.Send(new GetWholesalerInventoryQuery(id));
+
+ return Ok(wholesalerInventory);
+ }
+ catch (Exception ex)
+ {
+ return _exceptionHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/BrewABear/Api/Dockerfile b/BrewABear/Api/Dockerfile
new file mode 100644
index 0000000..759339c
--- /dev/null
+++ b/BrewABear/Api/Dockerfile
@@ -0,0 +1,28 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Api/Api.csproj", "Api/"]
+COPY ["Application/Application.csproj", "Application/"]
+COPY ["Data/Data.csproj", "Data/"]
+COPY ["Domain/Domain.csproj", "Domain/"]
+RUN dotnet restore "./Api/Api.csproj"
+COPY . .
+WORKDIR "/src/Api"
+RUN dotnet build "./Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Api.dll"]
\ No newline at end of file
diff --git a/BrewABear/Api/Program.cs b/BrewABear/Api/Program.cs
new file mode 100644
index 0000000..61645e6
--- /dev/null
+++ b/BrewABear/Api/Program.cs
@@ -0,0 +1,58 @@
+using Api.Services;
+using Application;
+using Application.Services;
+using Data;
+using Data.DataSeeding;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(opt => opt.SupportNonNullableReferenceTypes());
+
+var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
+
+if (string.IsNullOrEmpty(connectionString))
+{
+ throw new InvalidOperationException("Connection string could not be loaded from settings.");
+}
+
+builder.Services.AddDatabase(connectionString!);
+
+builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining());
+builder.Services.AddAutoMapper(cfg => cfg.AddProfile());
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddScoped();
+
+var app = builder.Build();
+
+app.UseSwagger();
+app.UseSwaggerUI();
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+var isTestMode = Array.Exists(AppDomain.CurrentDomain.GetAssemblies(),
+ a => a.FullName?.StartsWith("xunit", StringComparison.InvariantCultureIgnoreCase) ?? false);
+
+if (!isTestMode)
+{
+ using var scope = app.Services.CreateScope();
+
+ var seeder = scope.ServiceProvider.GetRequiredService();
+ seeder.ApplySeeding();
+}
+
+await app.RunAsync();
+
+#pragma warning disable S1118 // Utility classes should not have public constructors
+public partial class Program { }
+#pragma warning restore S1118 // Utility classes should not have public constructors
diff --git a/BrewABear/Api/Properties/launchSettings.json b/BrewABear/Api/Properties/launchSettings.json
new file mode 100644
index 0000000..dac80e7
--- /dev/null
+++ b/BrewABear/Api/Properties/launchSettings.json
@@ -0,0 +1,52 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5142"
+ },
+ "https": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:7183;http://localhost:5142"
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker",
+ "launchBrowser": true,
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
+ "environmentVariables": {
+ "ASPNETCORE_HTTPS_PORTS": "8081",
+ "ASPNETCORE_HTTP_PORTS": "8080"
+ },
+ "publishAllPorts": true,
+ "useSSL": true
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:48126",
+ "sslPort": 44357
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrewABear/Api/Services/ExceptionHandlerService.cs b/BrewABear/Api/Services/ExceptionHandlerService.cs
new file mode 100644
index 0000000..136f017
--- /dev/null
+++ b/BrewABear/Api/Services/ExceptionHandlerService.cs
@@ -0,0 +1,81 @@
+using Application.Exceptions;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Services;
+
+public class ExceptionHandlerService : IExceptionHandlerService
+{
+ public ActionResult HandleException(Exception exception)
+ {
+ return exception switch
+ {
+ BeerNotFoundException ex => CreateBeerNotFoundResult(ex),
+ BrewerNotFoundException ex => CreateBrewerNotFoundResult(ex),
+ BreweryNotFoundException ex => CreateBreweryNotFoundResult(ex),
+ OrderNotFoundException ex => CreateOrderNotFoundResult(ex),
+ WholesalerNotFoundException ex => CreateWholesalerNotFoundResult(ex),
+ ResourceNotFoundException ex => CreateNotFoundResult(ex),
+
+ NegativePriceException ex => CreateNegativePriceResult(ex),
+ NegativeQuantityException ex => CreateNegativeQuantityResult(ex),
+ _ => CreateGenericErrorResult(exception)
+ };
+ }
+
+ private static NotFoundObjectResult CreateNotFoundResult(ResourceNotFoundException exception)
+ {
+ var message = $"Resource with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static NotFoundObjectResult CreateBeerNotFoundResult(BeerNotFoundException exception)
+ {
+ var message = $"Beer with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static NotFoundObjectResult CreateBrewerNotFoundResult(BrewerNotFoundException exception)
+ {
+ var message = $"Brewer with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static NotFoundObjectResult CreateBreweryNotFoundResult(BreweryNotFoundException exception)
+ {
+ var message = $"Brewery with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static NotFoundObjectResult CreateOrderNotFoundResult(OrderNotFoundException exception)
+ {
+ var message = $"Order with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static NotFoundObjectResult CreateWholesalerNotFoundResult(WholesalerNotFoundException exception)
+ {
+ var message = $"Wholesaler with id '{exception.ResourceId}' was not found.";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static BadRequestObjectResult CreateNegativePriceResult(NegativePriceException exception)
+ {
+ var message = $"Price can't be less than 0. Was: {exception.Price}";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static BadRequestObjectResult CreateNegativeQuantityResult(NegativeQuantityException exception)
+ {
+ var message = $"Quantity can't be less than 0. Was: {exception.Quantity}";
+ return new(new ErrorDto(exception, message));
+ }
+
+ private static ObjectResult CreateGenericErrorResult(Exception exception)
+ {
+ var message = "An unexpected error occurred.";
+ return new ObjectResult(new ErrorDto(exception, message))
+ {
+ StatusCode = 500
+ };
+ }
+}
diff --git a/BrewABear/Api/Services/IExceptionHandlerService.cs b/BrewABear/Api/Services/IExceptionHandlerService.cs
new file mode 100644
index 0000000..b089879
--- /dev/null
+++ b/BrewABear/Api/Services/IExceptionHandlerService.cs
@@ -0,0 +1,7 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Services;
+public interface IExceptionHandlerService
+{
+ ActionResult HandleException(Exception exception);
+}
\ No newline at end of file
diff --git a/BrewABear/Api/appsettings.Development.json b/BrewABear/Api/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/BrewABear/Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/BrewABear/Api/appsettings.json b/BrewABear/Api/appsettings.json
new file mode 100644
index 0000000..0530a3f
--- /dev/null
+++ b/BrewABear/Api/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=mylovelydb.db;"
+ }
+}
diff --git a/BrewABear/Application/Application.csproj b/BrewABear/Application/Application.csproj
new file mode 100644
index 0000000..809f9d3
--- /dev/null
+++ b/BrewABear/Application/Application.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommand.cs b/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommand.cs
new file mode 100644
index 0000000..32ec26d
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.BrewerCommands;
+public record CreateBeerCommand(string BrewerId, BeerCreateDto Beer) : IRequest;
diff --git a/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommandHandler.cs b/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommandHandler.cs
new file mode 100644
index 0000000..5d28f80
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/CreateBeerCommandHandler.cs
@@ -0,0 +1,29 @@
+using Application.Exceptions;
+using Application.Services;
+
+namespace Application.Commands.BrewerCommands;
+internal class CreateBeerCommandHandler(DataContext context, IMapper mapper, IGuidCreator creator) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+ private readonly IGuidCreator _creator = creator;
+
+ public async Task Handle(CreateBeerCommand request, CancellationToken cancellationToken)
+ {
+ var brewer =
+ await _context.Brewers.FindAsync([request.BrewerId], cancellationToken: cancellationToken)
+ ?? throw new BrewerNotFoundException(request.BrewerId);
+
+ var newBeer = _mapper.Map(request.Beer);
+ if (newBeer.Price < 0)
+ throw new NegativePriceException(newBeer.Price);
+
+ newBeer.Brewer = brewer;
+ newBeer.Id = _creator.Create();
+
+ await _context.Beers.AddAsync(newBeer, cancellationToken);
+ await _context.SaveChangesAsync(cancellationToken);
+
+ return _mapper.Map(newBeer);
+ }
+}
diff --git a/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommand.cs b/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommand.cs
new file mode 100644
index 0000000..44a4158
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.BrewerCommands;
+public record DeleteBeerCommand(string BrewerId, string BeerId) : IRequest;
diff --git a/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommandHandler.cs b/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommandHandler.cs
new file mode 100644
index 0000000..f415761
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/DeleteBeerCommandHandler.cs
@@ -0,0 +1,22 @@
+using Application.Exceptions;
+
+namespace Application.Commands.BrewerCommands;
+internal class DeleteBeerCommandHandler(DataContext context) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+
+ public async Task Handle(DeleteBeerCommand request, CancellationToken cancellationToken)
+ {
+ var beer =
+ await _context.Beers.FindAsync([request.BeerId], cancellationToken: cancellationToken)
+ ?? throw new BeerNotFoundException(request.BeerId);
+
+ if (beer.BrewerId != request.BrewerId)
+ throw new BrewerNotFoundException(request.BrewerId);
+
+ _context.Beers.Remove(beer);
+ await _context.SaveChangesAsync(cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommand.cs b/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommand.cs
new file mode 100644
index 0000000..7267f08
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.BrewerCommands;
+public record UpdateBeerCommand(string BrewerId, string BeerId, BeerCreateDto Beer) : IRequest;
diff --git a/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommandHandler.cs b/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommandHandler.cs
new file mode 100644
index 0000000..a9d1dc4
--- /dev/null
+++ b/BrewABear/Application/Commands/BrewerCommands/UpdateBeerCommandHandler.cs
@@ -0,0 +1,29 @@
+using Application.Exceptions;
+
+namespace Application.Commands.BrewerCommands;
+internal class UpdateBeerCommandHandler(DataContext context, IMapper mapper) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task Handle(UpdateBeerCommand request, CancellationToken cancellationToken)
+ {
+ var beer =
+ await _context.Beers.FindAsync([request.BeerId], cancellationToken: cancellationToken)
+ ?? throw new BeerNotFoundException(request.BeerId);
+
+ if (beer.BrewerId != request.BrewerId)
+ throw new BrewerNotFoundException(request.BrewerId);
+
+ if (request.Beer.Price < 0)
+ throw new NegativePriceException(request.Beer.Price);
+
+ beer.Name = request.Beer.Name;
+ beer.Flavor = request.Beer.Flavor;
+ beer.Description = request.Beer.Description;
+ beer.Price = request.Beer.Price;
+
+ await _context.SaveChangesAsync(cancellationToken);
+ return _mapper.Map(beer);
+ }
+}
diff --git a/BrewABear/Application/Commands/OrderCommands/CreateOrderCommand.cs b/BrewABear/Application/Commands/OrderCommands/CreateOrderCommand.cs
new file mode 100644
index 0000000..c2d0ff2
--- /dev/null
+++ b/BrewABear/Application/Commands/OrderCommands/CreateOrderCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.OrderCommands;
+public record CreateOrderCommand(OrderCreateDto OrderCreateDto) : IRequest;
diff --git a/BrewABear/Application/Commands/OrderCommands/CreateOrderCommandHandler.cs b/BrewABear/Application/Commands/OrderCommands/CreateOrderCommandHandler.cs
new file mode 100644
index 0000000..8cbea74
--- /dev/null
+++ b/BrewABear/Application/Commands/OrderCommands/CreateOrderCommandHandler.cs
@@ -0,0 +1,41 @@
+using Application.Exceptions;
+using Application.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Commands.OrderCommands;
+internal class CreateOrderCommandHandler(DataContext context, IMapper mapper, IOrderService orderService, IGuidCreator creator) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+ private readonly IOrderService _orderService = orderService;
+ private readonly IGuidCreator _creator = creator;
+
+ public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
+ {
+ var order = _mapper.Map(request.OrderCreateDto);
+ if (order.Quantity < 0)
+ throw new NegativeQuantityException(order.Quantity);
+
+ var wholesalerCount = await _context.Wholesalers
+ .Where(f => f.Id == order.WholesalerId)
+ .CountAsync(cancellationToken);
+
+ if (wholesalerCount < 1)
+ throw new WholesalerNotFoundException(order.WholesalerId);
+
+ var beer = await _context.Beers
+ .FindAsync([order.BeerId], cancellationToken: cancellationToken)
+ ?? throw new BeerNotFoundException(order.BeerId);
+
+ order.PricePerBear = beer.Price;
+ order.Id = _creator.Create();
+
+ await _context.Orders.AddAsync(order, cancellationToken);
+ await _context.SaveChangesAsync(cancellationToken);
+
+ var dto = _mapper.Map(order);
+ dto.FinalPrice = _orderService.GetFinalPrice(order);
+
+ return dto;
+ }
+}
diff --git a/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommand.cs b/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommand.cs
new file mode 100644
index 0000000..cc77e6c
--- /dev/null
+++ b/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.OrderCommands;
+public record RequestQuoteCommand(string OrderId) : IRequest;
diff --git a/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommandHandler.cs b/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommandHandler.cs
new file mode 100644
index 0000000..a75b288
--- /dev/null
+++ b/BrewABear/Application/Commands/OrderCommands/RequestQuoteCommandHandler.cs
@@ -0,0 +1,30 @@
+using Application.Exceptions;
+using Application.Services;
+
+namespace Application.Commands.OrderCommands;
+internal class RequestQuoteCommandHandler(DataContext context, IOrderService orderService) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IOrderService _orderService = orderService;
+
+ public async Task Handle(RequestQuoteCommand request, CancellationToken cancellationToken)
+ {
+ var order = await _context.Orders
+ .FindAsync([request.OrderId], cancellationToken: cancellationToken)
+ ?? throw new OrderNotFoundException(request.OrderId);
+
+ order.DiscountPercentage = _orderService.GetQuotaPercentage(order);
+
+ await _context.SaveChangesAsync(cancellationToken);
+
+ var info = new QuoteInfoDto()
+ {
+ IsSuccessful = order.DiscountPercentage > 0,
+ OrderId = request.OrderId,
+ QuotePercentage = order.DiscountPercentage,
+ NewTotalPrice = _orderService.GetFinalPrice(order)
+ };
+
+ return info;
+ }
+}
diff --git a/BrewABear/Application/Commands/SaleCommands/CreateSaleCommand.cs b/BrewABear/Application/Commands/SaleCommands/CreateSaleCommand.cs
new file mode 100644
index 0000000..1636cdb
--- /dev/null
+++ b/BrewABear/Application/Commands/SaleCommands/CreateSaleCommand.cs
@@ -0,0 +1,2 @@
+namespace Application.Commands.SaleCommands;
+public record CreateSaleCommand(string WholesalerId, string BeerId, int quantity) : IRequest;
diff --git a/BrewABear/Application/Commands/SaleCommands/CreateSaleCommandHandler.cs b/BrewABear/Application/Commands/SaleCommands/CreateSaleCommandHandler.cs
new file mode 100644
index 0000000..745aeef
--- /dev/null
+++ b/BrewABear/Application/Commands/SaleCommands/CreateSaleCommandHandler.cs
@@ -0,0 +1,41 @@
+using Application.Exceptions;
+using Application.Services;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Application.Commands.SaleCommands;
+internal class CreateSaleCommandHandler(DataContext context, ISaleService saleService) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly ISaleService _saleService = saleService;
+
+ public async Task Handle(CreateSaleCommand request, CancellationToken cancellationToken)
+ {
+ if (request.quantity < 0)
+ throw new NegativeQuantityException(request.quantity);
+
+ var wholesaler = await _context.Wholesalers
+ .FindAsync([request.WholesalerId], cancellationToken: cancellationToken)
+ ?? throw new WholesalerNotFoundException(request.WholesalerId);
+
+ var beer = await _context.Beers
+ .FindAsync([request.BeerId], cancellationToken: cancellationToken)
+ ?? throw new BeerNotFoundException(request.BeerId);
+
+ var inventories = await _context.WholesalerInventories
+ .Where(f => f.WholesalerId == request.WholesalerId)
+ .ToListAsync(cancellationToken);
+
+ var item = _saleService.CreateSale(inventories, wholesaler, beer, request.quantity);
+ if (item is not null)
+ await _context.WholesalerInventories.AddAsync(item, cancellationToken);
+
+ await _context.SaveChangesAsync(cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/BrewABear/Application/DTOs/BeerCreateDto.cs b/BrewABear/Application/DTOs/BeerCreateDto.cs
new file mode 100644
index 0000000..6978620
--- /dev/null
+++ b/BrewABear/Application/DTOs/BeerCreateDto.cs
@@ -0,0 +1,8 @@
+namespace Application.DTOs;
+public class BeerCreateDto
+{
+ public string Name { get; set; } = default!;
+ public string Flavor { get; set; } = default!;
+ public string? Description { get; set; } = default;
+ public decimal Price { get; set; } = default;
+}
diff --git a/BrewABear/Application/DTOs/BeerDto.cs b/BrewABear/Application/DTOs/BeerDto.cs
new file mode 100644
index 0000000..cb65477
--- /dev/null
+++ b/BrewABear/Application/DTOs/BeerDto.cs
@@ -0,0 +1,10 @@
+namespace Application.DTOs;
+public class BeerDto
+{
+ public string Id { get; set; } = default!;
+ public string BrewerId { get; set; } = default!;
+ public string Name { get; set; } = default!;
+ public string Flavor { get; set; } = default!;
+ public string? Description { get; set; } = default;
+ public decimal Price { get; set; } = default;
+}
diff --git a/BrewABear/Application/DTOs/BrewerDto.cs b/BrewABear/Application/DTOs/BrewerDto.cs
new file mode 100644
index 0000000..5c428c0
--- /dev/null
+++ b/BrewABear/Application/DTOs/BrewerDto.cs
@@ -0,0 +1,9 @@
+namespace Application.DTOs;
+public class BrewerDto
+{
+ public string Id { get; set; } = default!;
+ public string BreweryId { get; set; } = default!;
+ public string FirstName { get; set; } = default!;
+ public string? LastName { get; set; } = default;
+ public string ContactEmail { get; set; } = default!;
+}
diff --git a/BrewABear/Application/DTOs/BreweryDto.cs b/BrewABear/Application/DTOs/BreweryDto.cs
new file mode 100644
index 0000000..867a265
--- /dev/null
+++ b/BrewABear/Application/DTOs/BreweryDto.cs
@@ -0,0 +1,7 @@
+namespace Application.DTOs;
+public class BreweryDto
+{
+ public string Id { get; set; } = default!;
+ public string Name { get; set; } = default!;
+ public string Address { get; set; } = default!;
+}
diff --git a/BrewABear/Application/DTOs/OrderCreateDto.cs b/BrewABear/Application/DTOs/OrderCreateDto.cs
new file mode 100644
index 0000000..ea72b0e
--- /dev/null
+++ b/BrewABear/Application/DTOs/OrderCreateDto.cs
@@ -0,0 +1,8 @@
+namespace Application.DTOs;
+public class OrderCreateDto
+{
+ public string ClientEmail { get; set; } = default!;
+ public string BeerId { get; set; } = default!;
+ public string WholesalerId { get; set; } = default!;
+ public int Quantity { get; set; }
+}
diff --git a/BrewABear/Application/DTOs/OrderDto.cs b/BrewABear/Application/DTOs/OrderDto.cs
new file mode 100644
index 0000000..376bc97
--- /dev/null
+++ b/BrewABear/Application/DTOs/OrderDto.cs
@@ -0,0 +1,12 @@
+namespace Application.DTOs;
+public class OrderDto
+{
+ public string Id { get; set; } = default!;
+ public string ClientEmail { get; set; } = default!;
+ public string BeerId { get; set; } = default!;
+ public string WholesalerId { get; set; } = default!;
+ public int Quantity { get; set; }
+ public decimal PricePerBear { get; set; }
+ public decimal DiscountPercentage { get; set; }
+ public decimal FinalPrice { get; set; }
+}
diff --git a/BrewABear/Application/DTOs/QuoteInfoDto.cs b/BrewABear/Application/DTOs/QuoteInfoDto.cs
new file mode 100644
index 0000000..5f0eaed
--- /dev/null
+++ b/BrewABear/Application/DTOs/QuoteInfoDto.cs
@@ -0,0 +1,8 @@
+namespace Application.DTOs;
+public class QuoteInfoDto
+{
+ public bool IsSuccessful { get; set; }
+ public string OrderId { get; set; } = default!;
+ public decimal QuotePercentage { get; set; }
+ public decimal NewTotalPrice { get; set; }
+}
diff --git a/BrewABear/Application/DTOs/WholesalerDto.cs b/BrewABear/Application/DTOs/WholesalerDto.cs
new file mode 100644
index 0000000..e1e2d6e
--- /dev/null
+++ b/BrewABear/Application/DTOs/WholesalerDto.cs
@@ -0,0 +1,6 @@
+namespace Application.DTOs;
+public class WholesalerDto
+{
+ public string Id { get; set; } = default!;
+ public string Name { get; set; } = default!;
+}
diff --git a/BrewABear/Application/DTOs/WholesalerInventoryDto.cs b/BrewABear/Application/DTOs/WholesalerInventoryDto.cs
new file mode 100644
index 0000000..53cb74e
--- /dev/null
+++ b/BrewABear/Application/DTOs/WholesalerInventoryDto.cs
@@ -0,0 +1,7 @@
+namespace Application.DTOs;
+public class WholesalerInventoryDto
+{
+ public string BeerId { get; set; } = default!;
+ public int Quantity { get; set; }
+ public decimal FixedPrice { get; set; }
+}
diff --git a/BrewABear/Application/Exceptions/BeerNotFoundException.cs b/BrewABear/Application/Exceptions/BeerNotFoundException.cs
new file mode 100644
index 0000000..1f40a92
--- /dev/null
+++ b/BrewABear/Application/Exceptions/BeerNotFoundException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class BeerNotFoundException(string beerId) : ResourceNotFoundException(beerId);
diff --git a/BrewABear/Application/Exceptions/BrewABeerException.cs b/BrewABear/Application/Exceptions/BrewABeerException.cs
new file mode 100644
index 0000000..9201f4f
--- /dev/null
+++ b/BrewABear/Application/Exceptions/BrewABeerException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class BrewABeerException : Exception;
diff --git a/BrewABear/Application/Exceptions/BrewerNotFoundException.cs b/BrewABear/Application/Exceptions/BrewerNotFoundException.cs
new file mode 100644
index 0000000..5a320eb
--- /dev/null
+++ b/BrewABear/Application/Exceptions/BrewerNotFoundException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class BrewerNotFoundException(string brewerId) : ResourceNotFoundException(brewerId);
diff --git a/BrewABear/Application/Exceptions/BreweryNotFoundException.cs b/BrewABear/Application/Exceptions/BreweryNotFoundException.cs
new file mode 100644
index 0000000..b66f6a0
--- /dev/null
+++ b/BrewABear/Application/Exceptions/BreweryNotFoundException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class BreweryNotFoundException(string breweryId) : ResourceNotFoundException(breweryId);
diff --git a/BrewABear/Application/Exceptions/ErrorDto.cs b/BrewABear/Application/Exceptions/ErrorDto.cs
new file mode 100644
index 0000000..e28058e
--- /dev/null
+++ b/BrewABear/Application/Exceptions/ErrorDto.cs
@@ -0,0 +1,6 @@
+namespace Application.Exceptions;
+public class ErrorDto(Exception exception, string message)
+{
+ public string Exception { get; set; } = exception.GetType().Name;
+ public string Message { get; set; } = message;
+}
\ No newline at end of file
diff --git a/BrewABear/Application/Exceptions/IAmTeapotException.cs b/BrewABear/Application/Exceptions/IAmTeapotException.cs
new file mode 100644
index 0000000..b24147a
--- /dev/null
+++ b/BrewABear/Application/Exceptions/IAmTeapotException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class PermanentlyTeapotException : BrewABeerException;
diff --git a/BrewABear/Application/Exceptions/NegativePriceException.cs b/BrewABear/Application/Exceptions/NegativePriceException.cs
new file mode 100644
index 0000000..622f1f2
--- /dev/null
+++ b/BrewABear/Application/Exceptions/NegativePriceException.cs
@@ -0,0 +1,5 @@
+namespace Application.Exceptions;
+public class NegativePriceException(decimal price) : BrewABeerException
+{
+ public decimal Price { get; set; } = price;
+}
diff --git a/BrewABear/Application/Exceptions/NegativeQuantityException.cs b/BrewABear/Application/Exceptions/NegativeQuantityException.cs
new file mode 100644
index 0000000..b43f691
--- /dev/null
+++ b/BrewABear/Application/Exceptions/NegativeQuantityException.cs
@@ -0,0 +1,5 @@
+namespace Application.Exceptions;
+public class NegativeQuantityException(int quantity) : BrewABeerException
+{
+ public int Quantity { get; set; } = quantity;
+}
diff --git a/BrewABear/Application/Exceptions/OrderNotFoundException.cs b/BrewABear/Application/Exceptions/OrderNotFoundException.cs
new file mode 100644
index 0000000..b9fd268
--- /dev/null
+++ b/BrewABear/Application/Exceptions/OrderNotFoundException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class OrderNotFoundException(string orderId) : ResourceNotFoundException(orderId);
diff --git a/BrewABear/Application/Exceptions/ResourceNotFoundException.cs b/BrewABear/Application/Exceptions/ResourceNotFoundException.cs
new file mode 100644
index 0000000..0b7812f
--- /dev/null
+++ b/BrewABear/Application/Exceptions/ResourceNotFoundException.cs
@@ -0,0 +1,5 @@
+namespace Application.Exceptions;
+public class ResourceNotFoundException(string resourceId) : BrewABeerException
+{
+ public string ResourceId { get; set; } = resourceId;
+}
diff --git a/BrewABear/Application/Exceptions/WholesalerNotFoundException.cs b/BrewABear/Application/Exceptions/WholesalerNotFoundException.cs
new file mode 100644
index 0000000..d8c71a9
--- /dev/null
+++ b/BrewABear/Application/Exceptions/WholesalerNotFoundException.cs
@@ -0,0 +1,2 @@
+namespace Application.Exceptions;
+public class WholesalerNotFoundException(string wholesalerId) : ResourceNotFoundException(wholesalerId);
diff --git a/BrewABear/Application/IStarter.cs b/BrewABear/Application/IStarter.cs
new file mode 100644
index 0000000..7b68bd8
--- /dev/null
+++ b/BrewABear/Application/IStarter.cs
@@ -0,0 +1,2 @@
+namespace Application;
+public interface IStarter;
diff --git a/BrewABear/Application/MappingProfiles.cs b/BrewABear/Application/MappingProfiles.cs
new file mode 100644
index 0000000..406e360
--- /dev/null
+++ b/BrewABear/Application/MappingProfiles.cs
@@ -0,0 +1,19 @@
+namespace Application;
+public class MappingProfiles : Profile
+{
+ public MappingProfiles()
+ {
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+ CreateMap();
+
+ CreateMap()
+ .ForMember(f => f.FixedPrice,
+ opt => opt.MapFrom(m => m.Beer.Price));
+
+ CreateMap();
+ CreateMap();
+ }
+}
diff --git a/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQuery.cs b/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQuery.cs
new file mode 100644
index 0000000..035eafd
--- /dev/null
+++ b/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BrewerQueries;
+public record GetAllBeersByBrewerQuery(string BrewerId) : IRequest>;
diff --git a/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQueryHandler.cs b/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQueryHandler.cs
new file mode 100644
index 0000000..00be482
--- /dev/null
+++ b/BrewABear/Application/Queries/BrewerQueries/GetAllBeersByBrewerQueryHandler.cs
@@ -0,0 +1,28 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.BrewerQueries;
+internal class GetAllBeersByBrewerQueryHandler(DataContext context, IMapper mapper) : IRequestHandler>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task> Handle(GetAllBeersByBrewerQuery request, CancellationToken cancellationToken)
+ {
+ var count = await _context.Brewers
+ .Where(f => f.Id == request.BrewerId)
+ .CountAsync(cancellationToken);
+
+ if (count == 0)
+ throw new ResourceNotFoundException(request.BrewerId);
+
+ IList beers = await _context.Beers
+ .Include(f => f.Brewer)
+ .Where(f => f.Brewer.Id == request.BrewerId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ return beers;
+ }
+}
diff --git a/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQuery.cs b/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQuery.cs
new file mode 100644
index 0000000..9e26a91
--- /dev/null
+++ b/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BrewerQueries;
+public record GetBrewerDetailsQuery(string BrewerId) : IRequest;
diff --git a/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQueryHandler.cs b/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQueryHandler.cs
new file mode 100644
index 0000000..7d8d59c
--- /dev/null
+++ b/BrewABear/Application/Queries/BrewerQueries/GetBrewerDetailsQueryHandler.cs
@@ -0,0 +1,21 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.BrewerQueries;
+internal class GetBrewerDetailsQueryHandler(DataContext context, IMapper mapper) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task Handle(GetBrewerDetailsQuery request, CancellationToken cancellationToken)
+ {
+ var brewer = await _context.Brewers
+ .Where(f => f.Id == request.BrewerId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .FirstOrDefaultAsync(cancellationToken)
+ ?? throw new BrewerNotFoundException(request.BrewerId);
+
+ return _mapper.Map(brewer);
+ }
+}
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQuery.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQuery.cs
new file mode 100644
index 0000000..9b9d32b
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BreweryQueries;
+public record GetAllBeersInBreweryQuery(string BreweryId) : IRequest?>;
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQueryHandler.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQueryHandler.cs
new file mode 100644
index 0000000..d61d749
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBeersInBreweryQueryHandler.cs
@@ -0,0 +1,29 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.BreweryQueries;
+internal class GetAllBeersInBreweryQueryHandler(DataContext context, IMapper mapper) : IRequestHandler?>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task?> Handle(GetAllBeersInBreweryQuery request, CancellationToken cancellationToken)
+ {
+ var count = await _context.Breweries
+ .Where(f => f.Id == request.BreweryId)
+ .CountAsync(cancellationToken);
+
+ if (count == 0)
+ throw new BreweryNotFoundException(request.BreweryId);
+
+ IList beers = await _context.Beers
+ .Include(f => f.Brewer)
+ .ThenInclude(f => f.Brewery)
+ .Where(f => f.Brewer.Brewery.Id == request.BreweryId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ return beers;
+ }
+}
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQuery.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQuery.cs
new file mode 100644
index 0000000..c517551
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BreweryQueries;
+public record GetAllBreweriesQuery : IRequest>;
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQueryHandler.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQueryHandler.cs
new file mode 100644
index 0000000..f5d18af
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBreweriesQueryHandler.cs
@@ -0,0 +1,15 @@
+using AutoMapper.QueryableExtensions;
+
+namespace Application.Queries.BreweryQueries;
+internal class GetAllBreweriesQueryHandler(DataContext context, IMapper mapper) : IRequestHandler>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public Task> Handle(GetAllBreweriesQuery request, CancellationToken cancellationToken)
+ {
+ IList breweries = [.. _context.Breweries.ProjectTo(_mapper.ConfigurationProvider)];
+
+ return Task.FromResult(breweries);
+ }
+}
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQuery.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQuery.cs
new file mode 100644
index 0000000..954db32
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BreweryQueries;
+public record GetAllBrewersInBreweryQuery(string BreweryId) : IRequest?>;
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQueryHandler.cs b/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQueryHandler.cs
new file mode 100644
index 0000000..d1b75a5
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetAllBrewersInBreweryQueryHandler.cs
@@ -0,0 +1,27 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.BreweryQueries;
+internal class GetAllBrewersInBreweryQueryHandler(DataContext context, IMapper mapper) : IRequestHandler?>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task?> Handle(GetAllBrewersInBreweryQuery request, CancellationToken cancellationToken)
+ {
+ var count = await _context.Breweries
+ .Where(f => f.Id == request.BreweryId)
+ .CountAsync(cancellationToken);
+
+ if (count == 0)
+ throw new BreweryNotFoundException(request.BreweryId);
+
+ IList brewers = await _context.Brewers
+ .Where(f => f.Brewery.Id == request.BreweryId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ return brewers;
+ }
+}
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQuery.cs b/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQuery.cs
new file mode 100644
index 0000000..2a1f3ee
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.BreweryQueries;
+public record GetBreweryDetailsQuery(string BreweryId) : IRequest;
diff --git a/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQueryHandler.cs b/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQueryHandler.cs
new file mode 100644
index 0000000..b059b9b
--- /dev/null
+++ b/BrewABear/Application/Queries/BreweryQueries/GetBreweryDetailsQueryHandler.cs
@@ -0,0 +1,21 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.BreweryQueries;
+internal class GetBreweryDetailsQueryHandler(DataContext context, IMapper mapper) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task Handle(GetBreweryDetailsQuery request, CancellationToken cancellationToken)
+ {
+ var brewery = await _context.Breweries
+ .Where(f => f.Id == request.BreweryId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .FirstOrDefaultAsync(cancellationToken)
+ ?? throw new BreweryNotFoundException(request.BreweryId);
+
+ return brewery;
+ }
+}
diff --git a/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQuery.cs b/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQuery.cs
new file mode 100644
index 0000000..3a63f17
--- /dev/null
+++ b/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.OrderQueries;
+public record GetAllOrdersQuery : IRequest>;
diff --git a/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQueryHandler.cs b/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQueryHandler.cs
new file mode 100644
index 0000000..7c9e98d
--- /dev/null
+++ b/BrewABear/Application/Queries/OrderQueries/GetAllOrdersQueryHandler.cs
@@ -0,0 +1,23 @@
+using Application.Services;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.OrderQueries;
+internal class GetAllOrdersQueryHandler(DataContext context, IMapper mapper, IOrderService orderService) : IRequestHandler>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+ private readonly IOrderService _orderService = orderService;
+
+ public async Task> Handle(GetAllOrdersQuery request, CancellationToken cancellationToken)
+ {
+ var orders = await _context.Orders
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ foreach (var order in orders)
+ order.FinalPrice = _orderService.GetFinalPrice(order);
+
+ return orders;
+ }
+}
diff --git a/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQuery.cs b/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQuery.cs
new file mode 100644
index 0000000..e9546c4
--- /dev/null
+++ b/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.OrderQueries;
+public record GetOrderDetailsQuery(string OrderId) : IRequest;
diff --git a/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQueryHandler.cs b/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQueryHandler.cs
new file mode 100644
index 0000000..27e34d4
--- /dev/null
+++ b/BrewABear/Application/Queries/OrderQueries/GetOrderDetailsQueryHandler.cs
@@ -0,0 +1,25 @@
+using Application.Exceptions;
+using Application.Services;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.OrderQueries;
+internal class GetOrderDetailsQueryHandler(DataContext context, IMapper mapper, IOrderService orderService) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+ private readonly IOrderService _orderService = orderService;
+
+ public async Task Handle(GetOrderDetailsQuery request, CancellationToken cancellationToken)
+ {
+ var order = await _context.Orders
+ .Where(f => f.Id == request.OrderId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .FirstOrDefaultAsync(cancellationToken)
+ ?? throw new OrderNotFoundException(request.OrderId);
+
+ order.FinalPrice = _orderService.GetFinalPrice(order);
+
+ return order;
+ }
+}
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQuery.cs b/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQuery.cs
new file mode 100644
index 0000000..8d0bebc
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.WholesalerQueries;
+public record GetAllWholesalersQuery : IRequest>;
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQueryHandler.cs b/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQueryHandler.cs
new file mode 100644
index 0000000..71f3eae
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetAllWholesalersQueryHandler.cs
@@ -0,0 +1,18 @@
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.WholesalerQueries;
+internal class GetAllWholesalersQueryHandler(DataContext context, IMapper mapper) : IRequestHandler>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task> Handle(GetAllWholesalersQuery request, CancellationToken cancellationToken)
+ {
+ IList wholesalers = await _context.Wholesalers
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ return wholesalers;
+ }
+}
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQuery.cs b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQuery.cs
new file mode 100644
index 0000000..fc9c872
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.WholesalerQueries;
+public record GetWholesalerDetailsQuery(string WholesalerId) : IRequest;
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQueryHandler.cs b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQueryHandler.cs
new file mode 100644
index 0000000..b070eaf
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerDetailsQueryHandler.cs
@@ -0,0 +1,21 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.WholesalerQueries;
+internal class GetWholesalerDetailsQueryHandler(DataContext context, IMapper mapper) : IRequestHandler
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task Handle(GetWholesalerDetailsQuery request, CancellationToken cancellationToken)
+ {
+ var wholesaler = await _context.Wholesalers
+ .Where(f => f.Id == request.WholesalerId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .FirstOrDefaultAsync(cancellationToken)
+ ?? throw new WholesalerNotFoundException(request.WholesalerId);
+
+ return wholesaler;
+ }
+}
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQuery.cs b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQuery.cs
new file mode 100644
index 0000000..71c2f6e
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQuery.cs
@@ -0,0 +1,2 @@
+namespace Application.Queries.WholesalerQueries;
+public record GetWholesalerInventoryQuery(string WholesalerId) : IRequest>;
diff --git a/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQueryHandler.cs b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQueryHandler.cs
new file mode 100644
index 0000000..d2d4595
--- /dev/null
+++ b/BrewABear/Application/Queries/WholesalerQueries/GetWholesalerInventoryQueryHandler.cs
@@ -0,0 +1,27 @@
+using Application.Exceptions;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Queries.WholesalerQueries;
+internal class GetWholesalerInventoryQueryHandler(DataContext context, IMapper mapper) : IRequestHandler>
+{
+ private readonly DataContext _context = context;
+ private readonly IMapper _mapper = mapper;
+
+ public async Task> Handle(GetWholesalerInventoryQuery request, CancellationToken cancellationToken)
+ {
+ var count = await _context.Wholesalers
+ .Where(f => f.Id == request.WholesalerId)
+ .CountAsync(cancellationToken);
+
+ if (count == 0)
+ throw new WholesalerNotFoundException(request.WholesalerId);
+
+ IList inventoryItems = await _context.WholesalerInventories
+ .Where(f => f.WholesalerId == request.WholesalerId)
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ return inventoryItems;
+ }
+}
diff --git a/BrewABear/Application/Services/GuidCreator.cs b/BrewABear/Application/Services/GuidCreator.cs
new file mode 100644
index 0000000..0eb804d
--- /dev/null
+++ b/BrewABear/Application/Services/GuidCreator.cs
@@ -0,0 +1,5 @@
+namespace Application.Services;
+public class GuidCreator : IGuidCreator
+{
+ public string Create() => Guid.NewGuid().ToString();
+}
diff --git a/BrewABear/Application/Services/IGuidCreator.cs b/BrewABear/Application/Services/IGuidCreator.cs
new file mode 100644
index 0000000..389bd91
--- /dev/null
+++ b/BrewABear/Application/Services/IGuidCreator.cs
@@ -0,0 +1,6 @@
+namespace Application.Services;
+
+public interface IGuidCreator
+{
+ string Create();
+}
\ No newline at end of file
diff --git a/BrewABear/Application/Services/IOrderService.cs b/BrewABear/Application/Services/IOrderService.cs
new file mode 100644
index 0000000..3a1a599
--- /dev/null
+++ b/BrewABear/Application/Services/IOrderService.cs
@@ -0,0 +1,9 @@
+
+namespace Application.Services;
+
+public interface IOrderService
+{
+ decimal GetFinalPrice(Order order);
+ decimal GetFinalPrice(OrderDto order);
+ decimal GetQuotaPercentage(Order order);
+}
\ No newline at end of file
diff --git a/BrewABear/Application/Services/ISaleService.cs b/BrewABear/Application/Services/ISaleService.cs
new file mode 100644
index 0000000..bf95ad8
--- /dev/null
+++ b/BrewABear/Application/Services/ISaleService.cs
@@ -0,0 +1,7 @@
+
+namespace Application.Services;
+
+public interface ISaleService
+{
+ WholesalerInventory? CreateSale(IList wholesalerInventories, Wholesaler wholesaler, Beer beer, int quantity);
+}
\ No newline at end of file
diff --git a/BrewABear/Application/Services/OrderService.cs b/BrewABear/Application/Services/OrderService.cs
new file mode 100644
index 0000000..9c60660
--- /dev/null
+++ b/BrewABear/Application/Services/OrderService.cs
@@ -0,0 +1,16 @@
+namespace Application.Services;
+public class OrderService : IOrderService
+{
+ public decimal GetFinalPrice(Order order) => order.Quantity * order.PricePerBear * (1 - order.DiscountPercentage);
+ public decimal GetFinalPrice(OrderDto order) => order.Quantity * order.PricePerBear * (1 - order.DiscountPercentage);
+
+ public decimal GetQuotaPercentage(Order order)
+ {
+ return order.Quantity switch
+ {
+ > 20 => .2M,
+ > 10 => .1M,
+ _ => 0
+ };
+ }
+}
diff --git a/BrewABear/Application/Services/SaleService.cs b/BrewABear/Application/Services/SaleService.cs
new file mode 100644
index 0000000..3c4342c
--- /dev/null
+++ b/BrewABear/Application/Services/SaleService.cs
@@ -0,0 +1,29 @@
+namespace Application.Services;
+public class SaleService : ISaleService
+{
+ public WholesalerInventory? CreateSale(
+ IList wholesalerInventories,
+ Wholesaler wholesaler,
+ Beer beer,
+ int quantity)
+ {
+ var item = wholesalerInventories.FirstOrDefault(f => f.BeerId == beer.Id);
+
+ if (item is null)
+ {
+ item = new()
+ {
+ BeerId = beer.Id,
+ WholesalerId = wholesaler.Id,
+ Quantity = quantity,
+ };
+
+ return item;
+ }
+ else
+ {
+ item.Quantity += quantity;
+ return null;
+ }
+ }
+}
diff --git a/BrewABear/BrewABear.sln b/BrewABear/BrewABear.sln
new file mode 100644
index 0000000..06377e9
--- /dev/null
+++ b/BrewABear/BrewABear.sln
@@ -0,0 +1,57 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35027.167
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api", "Api\Api.csproj", "{97B09179-CA45-4D89-A472-C7640F7B383A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "Domain\Domain.csproj", "{4BD4AE40-7F9B-42A2-8990-0CD3279E49EE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Data", "Data\Data.csproj", "{EE47DB7F-E8FE-4DF2-821C-B46AC18AA5CF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\Application.csproj", "{BB146294-3E9B-4868-AA42-B900F1AB3C67}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{69F684E7-04B0-4542-98D1-6F8E268A833A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{972FB709-C148-4C13-BD03-902B7505BB65}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F252A5AE-BEB9-4753-A4A2-146855529E46}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {97B09179-CA45-4D89-A472-C7640F7B383A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97B09179-CA45-4D89-A472-C7640F7B383A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97B09179-CA45-4D89-A472-C7640F7B383A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97B09179-CA45-4D89-A472-C7640F7B383A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4BD4AE40-7F9B-42A2-8990-0CD3279E49EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4BD4AE40-7F9B-42A2-8990-0CD3279E49EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4BD4AE40-7F9B-42A2-8990-0CD3279E49EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4BD4AE40-7F9B-42A2-8990-0CD3279E49EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EE47DB7F-E8FE-4DF2-821C-B46AC18AA5CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EE47DB7F-E8FE-4DF2-821C-B46AC18AA5CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE47DB7F-E8FE-4DF2-821C-B46AC18AA5CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EE47DB7F-E8FE-4DF2-821C-B46AC18AA5CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BB146294-3E9B-4868-AA42-B900F1AB3C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BB146294-3E9B-4868-AA42-B900F1AB3C67}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BB146294-3E9B-4868-AA42-B900F1AB3C67}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BB146294-3E9B-4868-AA42-B900F1AB3C67}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69F684E7-04B0-4542-98D1-6F8E268A833A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69F684E7-04B0-4542-98D1-6F8E268A833A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69F684E7-04B0-4542-98D1-6F8E268A833A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69F684E7-04B0-4542-98D1-6F8E268A833A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {972FB709-C148-4C13-BD03-902B7505BB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {972FB709-C148-4C13-BD03-902B7505BB65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {972FB709-C148-4C13-BD03-902B7505BB65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {972FB709-C148-4C13-BD03-902B7505BB65}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2D2CB65C-6316-4780-9B5C-66F6E4ABA930}
+ EndGlobalSection
+EndGlobal
diff --git a/BrewABear/Data/Data.csproj b/BrewABear/Data/Data.csproj
new file mode 100644
index 0000000..cefbd12
--- /dev/null
+++ b/BrewABear/Data/Data.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BrewABear/Data/DataContext.cs b/BrewABear/Data/DataContext.cs
new file mode 100644
index 0000000..e39fd34
--- /dev/null
+++ b/BrewABear/Data/DataContext.cs
@@ -0,0 +1,22 @@
+using Data.ModelConfigurations;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Data;
+public class DataContext(DbContextOptions options) : DbContext(options)
+{
+ public DbSet Beers { get; set; }
+ public DbSet BeerSales { get; set; }
+ public DbSet Brewers { get; set; }
+ public DbSet Breweries { get; set; }
+ public DbSet Orders { get; set; }
+ public DbSet Wholesalers { get; set; }
+ public DbSet WholesalerInventories { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ builder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly);
+ }
+}
diff --git a/BrewABear/Data/DataSeeding/DataSeeder.cs b/BrewABear/Data/DataSeeding/DataSeeder.cs
new file mode 100644
index 0000000..eed5e2e
--- /dev/null
+++ b/BrewABear/Data/DataSeeding/DataSeeder.cs
@@ -0,0 +1,186 @@
+using Domain.Models;
+
+namespace Data.DataSeeding;
+public class DataSeeder(DataContext context)
+{
+ private readonly DataContext _context = context;
+
+ public void ApplySeeding()
+ {
+ _context.Database.EnsureDeleted();
+ _context.Database.EnsureCreated();
+
+ IList breweries =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ Name = "Bear Brewery",
+ Address = "Prague, Big Street, 752/13",
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Name = "Alien Hot Brewery",
+ Address = "Mars, Small Crater, Loophole 17, 1",
+ }
+ ];
+
+ _context.Breweries.AddRange(breweries);
+
+ IList brewers =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ Brewery = breweries[0],
+ FirstName = "John",
+ LastName = "Wick",
+ ContactEmail = "john.superwick@superjohnwick.example.org"
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Brewery = breweries[1],
+ FirstName = "Pablo",
+ LastName = "Biden",
+ ContactEmail = "pablo1234qwerty@biden.example.org"
+ },
+ ];
+
+ _context.Brewers.AddRange(brewers);
+
+ IList beers =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ Brewer = brewers[0],
+ Name = "Quaternibeer",
+ Description = "Juicy beer, made for you.",
+ Flavor = "Moustache grape",
+ Price = 100,
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Brewer = brewers[0],
+ Name = "Bolsoyam",
+ Description = "Bean beer for Mr. Bean.",
+ Flavor = "Quirky apathy",
+ Price = 77,
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Brewer = brewers[1],
+ Name = "Big beer",
+ Description = "All you need for a day",
+ Flavor = "Strawberries",
+ Price = 222,
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Brewer = brewers[1],
+ Name = "PyPy",
+ Description = "Bites like a snake, feels like honey.",
+ Flavor = "Slow",
+ Price = 50,
+ }
+ ];
+
+ _context.Beers.AddRange(beers);
+
+ IList wholesalers =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ Name = "Branch Industries",
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Name = "Capybara Seasonal Inc.",
+ },
+ ];
+
+ _context.Wholesalers.AddRange(wholesalers);
+
+ IList beerSales =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ Beer = beers[0],
+ Quantity = 5,
+ Wholesaler = wholesalers[0],
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Beer = beers[1],
+ Quantity = 10,
+ Wholesaler = wholesalers[1],
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ Beer = beers[2],
+ Quantity = 7,
+ Wholesaler = wholesalers[1],
+ },
+ ];
+
+ _context.BeerSales.AddRange(beerSales);
+
+ IList orders =
+ [
+ new()
+ {
+ Id = CreateGuid(),
+ ClientEmail = "AbcSuperEmail@abc.example.org",
+ Beer = beers[0],
+ PricePerBear = 20,
+ DiscountPercentage = .1M,
+ Quantity = 25,
+ Wholesaler = wholesalers[0],
+ },
+ new()
+ {
+ Id = CreateGuid(),
+ ClientEmail = "AbsSuperEmail@abc.example.org",
+ Beer = beers[1],
+ PricePerBear = 89,
+ DiscountPercentage = .2M,
+ Quantity = 33,
+ Wholesaler = wholesalers[1],
+ },
+ ];
+
+ _context.Orders.AddRange(orders);
+
+ IList wholesalerInventories =
+ [
+ new()
+ {
+ Beer = beers[0],
+ Wholesaler = wholesalers[0],
+ Quantity = 5,
+ },
+ new()
+ {
+ Beer = beers[2],
+ Wholesaler = wholesalers[1],
+ Quantity = 7,
+ },
+ ];
+
+ _context.WholesalerInventories.AddRange(wholesalerInventories);
+
+ _context.SaveChanges();
+ }
+
+ private static string CreateGuid() => Guid.NewGuid().ToString();
+}
diff --git a/BrewABear/Data/DependencyInjection.cs b/BrewABear/Data/DependencyInjection.cs
new file mode 100644
index 0000000..8f2900d
--- /dev/null
+++ b/BrewABear/Data/DependencyInjection.cs
@@ -0,0 +1,17 @@
+using Data.DataSeeding;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Data;
+public static class DependencyInjection
+{
+ public static void AddDatabase(this IServiceCollection services, string connectionString)
+ {
+ services.AddDbContext(options =>
+ {
+ options.UseSqlite(connectionString);
+ });
+
+ services.AddScoped();
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/BeerConfiguration.cs b/BrewABear/Data/ModelConfigurations/BeerConfiguration.cs
new file mode 100644
index 0000000..0796976
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/BeerConfiguration.cs
@@ -0,0 +1,19 @@
+using Domain;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class BeerConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+ builder.Property(f => f.Name).HasMaxLength(DomainConfig.NameLength);
+ builder.Property(f => f.Flavor).HasMaxLength(DomainConfig.NameLength);
+
+ builder
+ .HasOne(f => f.Brewer)
+ .WithMany(f => f.Beers);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/BeerSaleConfiguration.cs b/BrewABear/Data/ModelConfigurations/BeerSaleConfiguration.cs
new file mode 100644
index 0000000..4c95110
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/BeerSaleConfiguration.cs
@@ -0,0 +1,15 @@
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class BeerSaleConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+
+ builder.HasOne(f => f.Beer);
+ builder.HasOne(f => f.Wholesaler);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/BrewerConfiguration.cs b/BrewABear/Data/ModelConfigurations/BrewerConfiguration.cs
new file mode 100644
index 0000000..e64391a
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/BrewerConfiguration.cs
@@ -0,0 +1,25 @@
+using Domain;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class BrewerConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+
+ builder.Property(f => f.FirstName).HasMaxLength(DomainConfig.NameLength);
+ builder.Property(f => f.LastName).HasMaxLength(DomainConfig.NameLength);
+ builder.Property(f => f.ContactEmail).HasMaxLength(DomainConfig.NameLength);
+
+ builder
+ .HasMany(f => f.Beers)
+ .WithOne(f => f.Brewer);
+
+ builder
+ .HasOne(f => f.Brewery)
+ .WithMany(f => f.Brewers);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/BreweryConfiguration.cs b/BrewABear/Data/ModelConfigurations/BreweryConfiguration.cs
new file mode 100644
index 0000000..3217ca8
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/BreweryConfiguration.cs
@@ -0,0 +1,19 @@
+using Domain;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class BreweryConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+ builder.Property(f => f.Name).HasMaxLength(DomainConfig.NameLength);
+ builder.Property(f => f.Address).HasMaxLength(DomainConfig.AddressLength);
+
+ builder
+ .HasMany(f => f.Brewers)
+ .WithOne(f => f.Brewery);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/OrderConfiguration.cs b/BrewABear/Data/ModelConfigurations/OrderConfiguration.cs
new file mode 100644
index 0000000..f292669
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/OrderConfiguration.cs
@@ -0,0 +1,18 @@
+using Domain;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class OrderConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+
+ builder.Property(f => f.ClientEmail).HasMaxLength(DomainConfig.NameLength);
+
+ builder.HasOne(f => f.Beer);
+ builder.HasOne(f => f.Wholesaler);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/WholesalerConfiguration.cs b/BrewABear/Data/ModelConfigurations/WholesalerConfiguration.cs
new file mode 100644
index 0000000..95e6ab6
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/WholesalerConfiguration.cs
@@ -0,0 +1,19 @@
+using Domain;
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class WholesalerConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => f.Id);
+
+ builder.Property(f => f.Name).HasMaxLength(DomainConfig.NameLength);
+
+ builder
+ .HasMany(f => f.InventoryItems)
+ .WithOne(f => f.Wholesaler);
+ }
+}
diff --git a/BrewABear/Data/ModelConfigurations/WholesalerInventoryConfiguration.cs b/BrewABear/Data/ModelConfigurations/WholesalerInventoryConfiguration.cs
new file mode 100644
index 0000000..536b5e9
--- /dev/null
+++ b/BrewABear/Data/ModelConfigurations/WholesalerInventoryConfiguration.cs
@@ -0,0 +1,18 @@
+using Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Data.ModelConfigurations;
+internal class WholesalerInventoryConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(f => new { f.BeerId, f.WholesalerId });
+
+ builder
+ .HasOne(f => f.Wholesaler)
+ .WithMany(f => f.InventoryItems);
+
+ builder.HasOne(f => f.Beer);
+ }
+}
diff --git a/BrewABear/Domain/Domain.csproj b/BrewABear/Domain/Domain.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/BrewABear/Domain/Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/BrewABear/Domain/DomainConfig.cs b/BrewABear/Domain/DomainConfig.cs
new file mode 100644
index 0000000..5216142
--- /dev/null
+++ b/BrewABear/Domain/DomainConfig.cs
@@ -0,0 +1,6 @@
+namespace Domain;
+public static class DomainConfig
+{
+ public const int NameLength = 100;
+ public const int AddressLength = 300;
+}
diff --git a/BrewABear/Domain/Models/Beer.cs b/BrewABear/Domain/Models/Beer.cs
new file mode 100644
index 0000000..3772fa3
--- /dev/null
+++ b/BrewABear/Domain/Models/Beer.cs
@@ -0,0 +1,11 @@
+namespace Domain.Models;
+public class Beer
+{
+ public string Id { get; set; } = default!;
+ public Brewer Brewer { get; set; } = default!;
+ public string BrewerId { get; set; } = default!;
+ public string Name { get; set; } = default!;
+ public string Flavor { get; set; } = default!;
+ public string? Description { get; set; } = default;
+ public decimal Price { get; set; } = default;
+}
diff --git a/BrewABear/Domain/Models/BeerSale.cs b/BrewABear/Domain/Models/BeerSale.cs
new file mode 100644
index 0000000..ec4b5ae
--- /dev/null
+++ b/BrewABear/Domain/Models/BeerSale.cs
@@ -0,0 +1,8 @@
+namespace Domain.Models;
+public class BeerSale
+{
+ public string Id { get; set; } = default!;
+ public Beer Beer { get; set; } = default!;
+ public Wholesaler Wholesaler { get; set; } = default!;
+ public int Quantity { get; set; }
+}
diff --git a/BrewABear/Domain/Models/Brewer.cs b/BrewABear/Domain/Models/Brewer.cs
new file mode 100644
index 0000000..315ed26
--- /dev/null
+++ b/BrewABear/Domain/Models/Brewer.cs
@@ -0,0 +1,11 @@
+namespace Domain.Models;
+public class Brewer
+{
+ public string Id { get; set; } = default!;
+ public Brewery Brewery { get; set; } = default!;
+ public string FirstName { get; set; } = default!;
+ public string? LastName { get; set; } = default;
+ public string ContactEmail { get; set; } = default!;
+
+ public IList Beers { get; set; } = [];
+}
diff --git a/BrewABear/Domain/Models/Brewery.cs b/BrewABear/Domain/Models/Brewery.cs
new file mode 100644
index 0000000..4d2b67a
--- /dev/null
+++ b/BrewABear/Domain/Models/Brewery.cs
@@ -0,0 +1,9 @@
+namespace Domain.Models;
+public class Brewery
+{
+ public string Id { get; set; } = default!;
+ public string Name { get; set; } = default!;
+ public string Address { get; set; } = default!;
+
+ public IList Brewers { get; set; } = [];
+}
diff --git a/BrewABear/Domain/Models/Order.cs b/BrewABear/Domain/Models/Order.cs
new file mode 100644
index 0000000..d07d483
--- /dev/null
+++ b/BrewABear/Domain/Models/Order.cs
@@ -0,0 +1,13 @@
+namespace Domain.Models;
+public class Order
+{
+ public string Id { get; set; } = default!;
+ public string ClientEmail { get; set; } = default!;
+ public Beer Beer { get; set; } = default!;
+ public string BeerId { get; set; } = default!;
+ public Wholesaler Wholesaler { get; set; } = default!;
+ public string WholesalerId { get; set; } = default!;
+ public int Quantity { get; set; }
+ public decimal PricePerBear { get; set; }
+ public decimal DiscountPercentage { get; set; }
+}
diff --git a/BrewABear/Domain/Models/Wholesaler.cs b/BrewABear/Domain/Models/Wholesaler.cs
new file mode 100644
index 0000000..d01bb79
--- /dev/null
+++ b/BrewABear/Domain/Models/Wholesaler.cs
@@ -0,0 +1,8 @@
+namespace Domain.Models;
+public class Wholesaler
+{
+ public string Id { get; set; } = default!;
+ public string Name { get; set; } = default!;
+
+ public IList InventoryItems { get; set; } = [];
+}
diff --git a/BrewABear/Domain/Models/WholesalerInventory.cs b/BrewABear/Domain/Models/WholesalerInventory.cs
new file mode 100644
index 0000000..762ff52
--- /dev/null
+++ b/BrewABear/Domain/Models/WholesalerInventory.cs
@@ -0,0 +1,9 @@
+namespace Domain.Models;
+public class WholesalerInventory
+{
+ public Wholesaler Wholesaler { get; set; } = default!;
+ public string WholesalerId { get; set; } = default!;
+ public Beer Beer { get; set; } = default!;
+ public string BeerId { get; set; } = default!;
+ public int Quantity { get; set; }
+}
diff --git a/BrewABear/IntegrationTests/ApiApplicationFactory.cs b/BrewABear/IntegrationTests/ApiApplicationFactory.cs
new file mode 100644
index 0000000..e04eb86
--- /dev/null
+++ b/BrewABear/IntegrationTests/ApiApplicationFactory.cs
@@ -0,0 +1,48 @@
+using Data;
+using Data.DataSeeding;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace IntegrationTests;
+public class ApiApplicationFactory : WebApplicationFactory
+{
+ private bool _disposed = false;
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureServices(services =>
+ {
+ services.RemoveAll();
+ services.RemoveAll>();
+
+ var connectionString = $"DataSource={Guid.NewGuid()};";
+ services.AddDatabase(connectionString);
+
+ var sp = services.BuildServiceProvider();
+
+ using var scope = sp.CreateScope();
+ var seeder = scope.ServiceProvider.GetRequiredService();
+ seeder.ApplySeeding();
+ });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ return;
+
+ if (disposing)
+ {
+ using var scope = Services.CreateScope();
+ var context = scope.ServiceProvider.GetRequiredService();
+ context.Database.EnsureDeleted();
+
+ _disposed = true;
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/BrewABear/IntegrationTests/BasicTests.cs b/BrewABear/IntegrationTests/BasicTests.cs
new file mode 100644
index 0000000..7e112c9
--- /dev/null
+++ b/BrewABear/IntegrationTests/BasicTests.cs
@@ -0,0 +1,37 @@
+using FluentAssertions;
+
+namespace IntegrationTests;
+public class BasicTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData("/Api/Wholesaler/All")]
+ [InlineData("/Api/Brewery/All")]
+ [InlineData("/Api/Order/All")]
+ public async Task Get_EndpointsReturnsSuccess(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync(url);
+
+ response.EnsureSuccessStatusCode();
+ response.Content.Headers.ContentType?.ToString().Should().Be("application/json; charset=utf-8");
+ }
+
+ [Theory]
+ [InlineData("/NotApi/123")]
+ [InlineData("/Api/Brewery/Nonexisting")]
+ [InlineData("/Api/Nonexisting")]
+ public async Task GetNonExisting_ReturnsNotFound(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync(url);
+
+ var body = await response.Content.ReadAsStringAsync();
+
+ response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
+ body.Should().Be(string.Empty);
+ }
+}
diff --git a/BrewABear/IntegrationTests/ControllerTests/BrewerControllerTests.cs b/BrewABear/IntegrationTests/ControllerTests/BrewerControllerTests.cs
new file mode 100644
index 0000000..b17fbea
--- /dev/null
+++ b/BrewABear/IntegrationTests/ControllerTests/BrewerControllerTests.cs
@@ -0,0 +1,97 @@
+using Application.DTOs;
+using FluentAssertions;
+using System.Net.Http.Json;
+
+namespace IntegrationTests.ControllerTests;
+public class BrewerControllerTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData("/Api/Brewer/{0}/Beers")]
+ public async Task GetBrewerBeers_ReturnsBeers(string url)
+ {
+ var client = _factory.CreateClient();
+ var brewer = await GetBrewer(client);
+
+ var response = await client.GetFromJsonAsync>(string.Format(url, brewer?.Id));
+
+ response.Should().NotBeNull();
+ response?.Count.Should().BeGreaterThan(0);
+ response?[0].Should().NotBeNull();
+ response?[0].Name.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewer/{0}/Details")]
+ public async Task GetBrewerDetails_ReturnsDetails(string url)
+ {
+ var client = _factory.CreateClient();
+ var brewer = await GetBrewer(client);
+
+ var response = await client.GetFromJsonAsync(string.Format(url, brewer?.Id));
+
+ response.Should().NotBeNull();
+ response?.Id.Should().BeEquivalentTo(brewer?.Id);
+ response?.FirstName.Should().NotBeNullOrWhiteSpace();
+ response?.ContactEmail.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewer/{0}/AddBeer")]
+ public async Task AddBeer_ReturnsCreatedBeer(string url)
+ {
+ var client = _factory.CreateClient();
+ var brewer = await GetBrewer(client);
+
+ var newBrewer = new BeerCreateDto()
+ {
+ Name = "Integration beer",
+ Description = "Beer used in integration testing",
+ Flavor = "Flavory",
+ Price = 10,
+ };
+
+ var response = await client.PostAsJsonAsync(string.Format(url, brewer?.Id), newBrewer);
+ var created = await response.Content.ReadFromJsonAsync();
+
+ response.EnsureSuccessStatusCode();
+ created.Should().NotBeNull();
+ created?.Id.Should().NotBeNullOrWhiteSpace();
+ created?.Name.Should().BeEquivalentTo(newBrewer.Name);
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewer/{0}/UpdateBeer?beerId={1}")]
+ public async Task UpdateBeer_ReturnsUpdatedBeer(string url)
+ {
+ var client = _factory.CreateClient();
+ var brewer = await GetBrewer(client);
+
+ var beers = await client.GetFromJsonAsync>(string.Format("/Api/Brewer/{0}/Beers", brewer?.Id));
+ var newBeer = new BeerCreateDto()
+ {
+ Name = "Modified",
+ Description = "Modified",
+ Flavor = "Modified",
+ Price = 1
+ };
+
+ var response = await client.PutAsJsonAsync(string.Format(url, brewer?.Id, beers?[0].Id), newBeer);
+ var created = await response.Content.ReadFromJsonAsync();
+
+ response.EnsureSuccessStatusCode();
+ created.Should().NotBeNull();
+ created?.Id.Should().NotBeNullOrWhiteSpace();
+ created?.Name.Should().BeEquivalentTo(newBeer.Name);
+ }
+
+ private static async Task GetBrewer(HttpClient client)
+ {
+ var breweries = await client.GetFromJsonAsync>("/Api/Brewery/All");
+ var brewery = breweries?[0];
+
+ var brewers = await client.GetFromJsonAsync>($"/Api/Brewery/{brewery?.Id}/Brewers");
+ return brewers?[0];
+ }
+}
diff --git a/BrewABear/IntegrationTests/ControllerTests/BreweryControllerTests.cs b/BrewABear/IntegrationTests/ControllerTests/BreweryControllerTests.cs
new file mode 100644
index 0000000..56a251c
--- /dev/null
+++ b/BrewABear/IntegrationTests/ControllerTests/BreweryControllerTests.cs
@@ -0,0 +1,73 @@
+using Application.DTOs;
+using FluentAssertions;
+using System.Net.Http.Json;
+
+namespace IntegrationTests.ControllerTests;
+public class BreweryControllerTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData("/Api/Brewery/All")]
+ public async Task GetAllBreweries_ReturnsObjects(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetFromJsonAsync>(url);
+
+ response.Should().NotBeNull();
+ response?[0].Should().NotBeNull();
+ response?[0].Id.Should().NotBeNull();
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewery/{0}/Details")]
+ public async Task GetBreweryDetails_ReturnsDetails(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var brewery = await GetBrewery(client);
+ var details = await client.GetFromJsonAsync(string.Format(url, brewery?.Id));
+
+ details.Should().NotBeNull();
+ details?.Id.Should().NotBeNullOrWhiteSpace();
+ details?.Name.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewery/{0}/Beers")]
+ public async Task GetBreweryBeers_ReturnsBeers(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var brewery = await GetBrewery(client);
+ var beers = await client.GetFromJsonAsync>(string.Format(url, brewery?.Id));
+
+ beers.Should().NotBeNull();
+ beers?.Count.Should().BeGreaterThan(0);
+ beers?[0].Should().NotBeNull();
+ beers?[0].Name.Should().NotBeNullOrWhiteSpace();
+ beers?[0].Id.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("/Api/Brewery/{0}/Brewers")]
+ public async Task GetBreweryBrewers_ReturnsBrewers(string url)
+ {
+ var client = _factory.CreateClient();
+
+ var brewery = await GetBrewery(client);
+ var brewers = await client.GetFromJsonAsync>(string.Format(url, brewery?.Id));
+
+ brewers.Should().NotBeNull();
+ brewers?.Count.Should().BeGreaterThan(0);
+ brewers?[0].Id.Should().NotBeNullOrWhiteSpace();
+ brewers?[0].FirstName.Should().NotBeNullOrWhiteSpace();
+ }
+
+ private static async Task GetBrewery(HttpClient client)
+ {
+ var breweries = await client.GetFromJsonAsync>("/Api/Brewery/All");
+ return breweries?[0];
+ }
+}
diff --git a/BrewABear/IntegrationTests/ControllerTests/OrderControllerTests.cs b/BrewABear/IntegrationTests/ControllerTests/OrderControllerTests.cs
new file mode 100644
index 0000000..a129275
--- /dev/null
+++ b/BrewABear/IntegrationTests/ControllerTests/OrderControllerTests.cs
@@ -0,0 +1,105 @@
+using Application.DTOs;
+using FluentAssertions;
+using System.Net.Http.Json;
+
+namespace IntegrationTests.ControllerTests;
+public class OrderControllerTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData("/Api/Order/All", "/Api/Order/{0}/Details")]
+ public async Task GetOrderEndpoints_ReturnOrders(string url1, string url2)
+ {
+ var client = _factory.CreateClient();
+
+ var orders = await client.GetFromJsonAsync>(url1);
+
+ orders.Should().NotBeNullOrEmpty();
+ var order = orders?[0];
+ order.Should().NotBeNull();
+ order?.Id.Should().NotBeNullOrWhiteSpace();
+ order?.ClientEmail.Should().NotBeNullOrWhiteSpace();
+
+ var orderDetailed = await client.GetFromJsonAsync(string.Format(url2, order?.Id));
+
+ orderDetailed.Should().NotBeNull();
+ orderDetailed.Should().BeEquivalentTo(order);
+ }
+
+ [Theory]
+ [InlineData(
+ "/Api/Wholesaler/All",
+ "/Api/Brewery/All",
+ "/Api/Brewery/{0}/Beers",
+ "/Api/Order/Add",
+ "/Api/Order/RequestQuote?orderid={0}")]
+ public async Task PostOrderEndpoints_ReturnData(string url1, string url2, string url3, string url4, string url5)
+ {
+ var client = _factory.CreateClient();
+
+ var wholesalers = await client.GetFromJsonAsync>(url1);
+ var saler = wholesalers?[0];
+
+ var breweries = await client.GetFromJsonAsync>(url2);
+ var brewery = breweries?[0];
+
+ var beers = await client.GetFromJsonAsync>(string.Format(url3, brewery?.Id));
+ var beer = beers?[0];
+
+ saler.Should().NotBeNull();
+ beer.Should().NotBeNull();
+
+ var order1 = new OrderCreateDto()
+ {
+ ClientEmail = "client1@example.org",
+ BeerId = beer!.Id,
+ WholesalerId = saler!.Id,
+ Quantity = 7
+ };
+
+ var response1 = await client.PostAsJsonAsync(url4, order1);
+ response1.EnsureSuccessStatusCode();
+ var result1 = await response1.Content.ReadFromJsonAsync();
+
+ result1.Should().NotBeNull();
+ result1?.ClientEmail.Should().BeEquivalentTo(order1.ClientEmail);
+ result1?.BeerId.Should().BeEquivalentTo(order1.BeerId);
+ result1?.WholesalerId.Should().BeEquivalentTo(order1.WholesalerId);
+ result1?.Quantity.Should().Be(order1.Quantity);
+
+ var order2 = new OrderCreateDto()
+ {
+ ClientEmail = "client2@example.org",
+ BeerId = beer!.Id,
+ WholesalerId = saler!.Id,
+ Quantity = 77
+ };
+
+ var response2 = await client.PostAsJsonAsync(url4, order2);
+ response2.EnsureSuccessStatusCode();
+ var result2 = await response2.Content.ReadFromJsonAsync();
+
+ result2.Should().NotBeNull();
+ result2?.ClientEmail.Should().BeEquivalentTo(order2.ClientEmail);
+ result2?.BeerId.Should().BeEquivalentTo(order2.BeerId);
+ result2?.WholesalerId.Should().BeEquivalentTo(order2.WholesalerId);
+ result2?.Quantity.Should().Be(order2.Quantity);
+
+ var quoteResponse1 = await client.PostAsJsonAsync(string.Format(url5, result1?.Id), new StringContent(string.Empty));
+ quoteResponse1.EnsureSuccessStatusCode();
+ var quoteResult1 = await quoteResponse1.Content.ReadFromJsonAsync();
+
+ quoteResult1.Should().NotBeNull();
+ quoteResult1?.IsSuccessful.Should().BeFalse();
+ quoteResult1?.OrderId.Should().BeEquivalentTo(result1?.Id);
+
+ var quoteResponse2 = await client.PostAsJsonAsync(string.Format(url5, result2?.Id), new StringContent(string.Empty));
+ quoteResponse2.EnsureSuccessStatusCode();
+ var quoteResult2 = await quoteResponse2.Content.ReadFromJsonAsync();
+
+ quoteResult2.Should().NotBeNull();
+ quoteResult2?.IsSuccessful.Should().BeTrue();
+ quoteResult2?.OrderId.Should().BeEquivalentTo(result2?.Id);
+ }
+}
diff --git a/BrewABear/IntegrationTests/ControllerTests/SaleControllerTests.cs b/BrewABear/IntegrationTests/ControllerTests/SaleControllerTests.cs
new file mode 100644
index 0000000..78e042e
--- /dev/null
+++ b/BrewABear/IntegrationTests/ControllerTests/SaleControllerTests.cs
@@ -0,0 +1,35 @@
+using Application.DTOs;
+using FluentAssertions;
+using System.Net.Http.Json;
+
+namespace IntegrationTests.ControllerTests;
+public class SaleControllerTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData(
+ "/Api/Wholesaler/All",
+ "/Api/Brewery/All",
+ "/Api/Brewery/{0}/Beers",
+ "/Api/Sale/Add?wholesalerId={0}&beerId={1}&quantity=10")]
+ public async Task SaleEndpoint_ReturnsSuccess(string url1, string url2, string url3, string url4)
+ {
+ var client = _factory.CreateClient();
+
+ var wholesalers = await client.GetFromJsonAsync>(url1);
+ var saler = wholesalers?[0];
+
+ var breweries = await client.GetFromJsonAsync>(url2);
+ var brewery = breweries?[0];
+
+ var beers = await client.GetFromJsonAsync>(string.Format(url3, brewery?.Id));
+ var beer = beers?[0];
+
+ var content = new StringContent(string.Empty);
+ var response = await client.PostAsync(string.Format(url4, saler?.Id, beer?.Id), content);
+
+ response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
+ (await response.Content.ReadAsStringAsync()).Should().BeNullOrWhiteSpace();
+ }
+}
diff --git a/BrewABear/IntegrationTests/ControllerTests/WholesalerControllerTests.cs b/BrewABear/IntegrationTests/ControllerTests/WholesalerControllerTests.cs
new file mode 100644
index 0000000..0396847
--- /dev/null
+++ b/BrewABear/IntegrationTests/ControllerTests/WholesalerControllerTests.cs
@@ -0,0 +1,33 @@
+using Application.DTOs;
+using FluentAssertions;
+using System.Net.Http.Json;
+
+namespace IntegrationTests.ControllerTests;
+public class WholesalerControllerTests(ApiApplicationFactory factory) : IClassFixture
+{
+ private readonly ApiApplicationFactory _factory = factory;
+
+ [Theory]
+ [InlineData("/Api/Wholesaler/All", "/Api/Wholesaler/{0}/Details", "/Api/Wholesaler/{0}/Inventory")]
+ public async Task WholesalerEndpoints_ReturnInformation(string url1, string url2, string url3)
+ {
+ var client = _factory.CreateClient();
+
+ var wholesalers = await client.GetFromJsonAsync>(url1);
+
+ wholesalers.Should().NotBeNullOrEmpty();
+
+ var saler = wholesalers?[0];
+ saler.Should().NotBeNull();
+
+ var detailed = await client.GetFromJsonAsync(string.Format(url2, saler?.Id));
+ detailed.Should().NotBeNull();
+ detailed?.Id.Should().BeEquivalentTo(saler?.Id);
+ detailed?.Name.Should().BeEquivalentTo(saler?.Name);
+
+ var inventory = await client.GetFromJsonAsync>(string.Format(url3, detailed?.Id));
+ inventory.Should().NotBeNullOrEmpty();
+ inventory?[0].BeerId.Should().NotBeNullOrWhiteSpace();
+ inventory?[0].Quantity.Should().BeGreaterThan(1);
+ }
+}
diff --git a/BrewABear/IntegrationTests/IntegrationTests.csproj b/BrewABear/IntegrationTests/IntegrationTests.csproj
new file mode 100644
index 0000000..e638f67
--- /dev/null
+++ b/BrewABear/IntegrationTests/IntegrationTests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BrewABear/LICENSE b/BrewABear/LICENSE
new file mode 100644
index 0000000..09f2b49
--- /dev/null
+++ b/BrewABear/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Sviatoslav Zubar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/BrewABear/README.md b/BrewABear/README.md
new file mode 100644
index 0000000..567b684
--- /dev/null
+++ b/BrewABear/README.md
@@ -0,0 +1,165 @@
+# BrewABear
+BrewABear is a comprehensive management system designed for breweries and wholesalers in the beer industry. This ASP.NET Core API provides a robust backend for tracking breweries, brewers, beers, wholesalers, sales, and orders. With features like inventory management, quote generation, and discount application, BrewABear streamlines operations for both beer producers and distributors. Built with clean architecture and CQRS pattern, it offers a scalable and maintainable solution for the craft beer ecosystem.
+
+Access API website: https://brewabear-fmehdeerdpfectgw.westeurope-01.azurewebsites.net/swagger/index.html
+
+
+ Technical API Requirements
+Welcome to Belgium! You've been contacted to create a management system for breweries and wholesalers. Below are listed the functional and technical requirements sent by your client
+
+## Functional Requirements
+ - [x] List all beers by brewery
+ - [x] A brewer can add, delete and update beers
+ - [x] Add the sale of an existing beer to an existing wholesaler
+ - [x] Upon a sale, the quantity of a beer needs to be incremented in the wholesaler's inventory
+ - [x] A client can request a quote from a wholesaler.
+ - [x] If successful, the quote returns a price and a summary of the quote. A 10% discount is applied for orders above 10 units. A 20% discount is applied for orders above 20 drinks.
+ - [x] If there is an error, it returns an exception and a message to explain the reason: order cannot be empty; wholesaler must exist; there can't be any duplicates in the order; the number of beers ordered cannot be greater than the wholesaler's stock; the beer must be sold by the wholesaler
+## Business Rules:
+ - [x] A brewer brews one or several beers
+ - [x] A beer is always linked to a brewer
+ - [x] A beer can be sold by several wholesalers
+ - [x] A wholesaler sells a defined list of beers, from any brewer, and has only a limited stock of those beers
+ - [x] The beers sold by the wholesaler have a fixed price imposed by the brewery
+ - [x] For this assessment, it is considered that all sales are made without tax
+ - [x] The database is pre-filled by you
+ - [x] No front-end is needed, just the API
+ - [x] Use REST architecture
+ - [x] Use Entity Framework
+ - [x] No migrations are needed; use Ensure Deleted and Ensure Created to facilitate development and code reviews.
+
+## Challenges:
+ - [x] Add unit tests to make sure business constraints are accurate.
+ - [x] Include a Read me with your thought process, your challenges and instructions on how to run the app.
+ - [x] Add integrations tests using a real test database. These will ensure data is still added corrected when the codebase changes. The test database must be created and deleted for each test.
+
+
+## Table of Contents
+- [Idea](#idea)
+- [Endpoints](#endpoints)
+- [Architecture](#architecture)
+- [Technologies](#technologies)
+- [Trying the API](#trying-the-api)
+ - [With hosted website](#with-hosted-website)
+ - [With Postman](#with-postman)
+ - [With local installation](#with-local-installation)
+- [CI / CD](#ci--cd)
+- [License](#license)
+- [Contact](#contact)
+
+## Idea
+The system will consist of 6 domain models:
+- Brewery
+- Brewer
+- Beer
+- Wholesaler
+- Beer Sale
+- Order
+
+Additional considerations:
+- Ids will be represented as a GUID object.
+- Database used: SQLite
+- Tests done with xUnit
+
+## Endpoints
+The general description of endpoints. Visit the api website to see actual complete swagger documentation.
+
+### Brewer
+- "Brewer/{id}/Beers" Get a list of all beers made by the brewer
+- "Brewer/{id}/Details" Get details about a specific brewer
+- "Brewer/{id}/AddBeer" Add a beer
+- "Brewer/{id}/DeleteBeer" Delete a beer
+- "Brewer/{id}/UpdateBeer" Update a beer
+
+### Brewery
+- "Brewery/All" Get a list of all breweries
+- "Brewery/{id}/Details" Get details about a specific brewery
+- "Brewery/{id}/Beers" Get a list of all beers in the brewery
+- "Brewery/{id}/Brewers" Get a list of all brewers in an brewery
+
+### Tea
+- "Tea/GetTea" Get tea
+- "Tea/GetCoffee" Get coffee
+
+### Order
+- "Order/All" Get a list of all orders
+- "Order/{id}/Details" Get details about a specific order
+- "Order/Add?orderDto" Make an order of some beers
+- "Order/RequestQuote?orderId" Request a quote from wholesaler
+
+### Sale
+- "Sale/Add?wholesalerid&beerid&quantity" Add a sale of a beer to wholesaler
+
+### Wholesaler
+- "Wholesaler/All" Get a list of wholesalers
+- "Wholesaler/{id}/Details" Get details about a specific wholesaler
+- "Wholesaler/{id}/Inventory" Get inventory of a specified wholesaler
+
+## Architecture
+- Clean architecture with CQRS pattern
+- Comprehensive Unit testing, Integration testing, API testing
+- Dependency Inversion for decoupling and easy extension
+- Adherence to design patterns and principles
+
+## Technologies
+- ASP.NET Core API
+- Entity Framework Core
+- SQLite
+- xUnit
+- Docker
+- Microsoft Azure
+
+## Trying the API
+
+### With hosted website
+The API is hosted on Microsoft Azure and can be accessed at any time by clicking [This link](https://brewabear-fmehdeerdpfectgw.westeurope-01.azurewebsites.net/swagger/index.html).
+The page contains a comprehensive swagger documentation, containing all endpoints, schemas, parameters, responses.
+Requests can be interactively sent using the UI.
+
+### With Postman
+The API features a Postman collection, containing request for every endpoint, which can be accessed [here](https://www.postman.com/riebi/workspace/brewabeer/collection/30063627-14a854e9-38f8-4076-aa84-6a5734b3eb67). Upon destination, click on three circles right to 'API Tests', and select 'Run collection'. All requests will be tested. Individual requests may be run too, if needed. Please keep in mind that a registered Postman account is required to run requests, which can be created for free.
+
+### With local installation
+Dotnet 8 SDK and runtime is needed to run the app locally, which can be downloaded at: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
+
+1. Clone the repository
+```bash
+git clone https://github.com/RieBi/BrewABear.git
+```
+2. Navigate to the project directory
+```bash
+cd BrewABear
+```
+3. Start the application
+```bash
+dotnet run --project Api
+```
+4. Navigate to the localhost at the port specified in the output. See below for details.
+
+For the following last lines of example output:
+```bash
+info: Microsoft.Hosting.Lifetime[14]
+ Now listening on: http://localhost:5142
+info: Microsoft.Hosting.Lifetime[0]
+ Application started. Press Ctrl+C to shut down.
+info: Microsoft.Hosting.Lifetime[0]
+ Hosting environment: Development
+info: Microsoft.Hosting.Lifetime[0]
+ Content root path: /root/demo/BrewABear/Api
+```
+The API would be hosted at http://localhost:5142.
+Therefore, navigating to http://localhost:5142/swagger will show you the swagger documentation.
+Navigating to http://localhost:5142/Api/Wholesaler/All will show the JSON response from your first request.
+
+## CI / CD
+The project uses separate workflows for Continuous Integration (CI) and Continuous Deployment (CD):
+
+- CI: Automatically builds and tests the project on each push or pull request.
+- CD: Automatically builds the project into a Docker container, uploads it to Docker Hub, and deploys it to run on Microsoft Azure hosting.
+This setup provides a seamless development experience and ease of use.
+
+## License
+This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.txt) file for details.
+
+## Contact
+For any questions or feedback, please open an issue on GitHub or contact the maintainer at riebisv@gmail.com.
diff --git a/BrewABear/Tests/Application/Services/GuidCreatorTests.cs b/BrewABear/Tests/Application/Services/GuidCreatorTests.cs
new file mode 100644
index 0000000..4f74b55
--- /dev/null
+++ b/BrewABear/Tests/Application/Services/GuidCreatorTests.cs
@@ -0,0 +1,27 @@
+using Application.Services;
+
+namespace Tests.Application.Services;
+public class GuidCreatorTests
+{
+ [Fact]
+ public void CreatesGuid()
+ {
+ var service = new GuidCreator();
+
+ var guid = service.Create();
+
+ Assert.NotNull(guid);
+ Assert.True(guid.Length > 10);
+ }
+
+ [Fact]
+ public void CreatesDifferentGuids()
+ {
+ var service = new GuidCreator();
+
+ var guid = service.Create();
+ var guid2 = service.Create();
+
+ Assert.NotEqual(guid, guid2);
+ }
+}
diff --git a/BrewABear/Tests/Application/Services/OrderServiceTests.cs b/BrewABear/Tests/Application/Services/OrderServiceTests.cs
new file mode 100644
index 0000000..9145dd8
--- /dev/null
+++ b/BrewABear/Tests/Application/Services/OrderServiceTests.cs
@@ -0,0 +1,47 @@
+using Application.Services;
+using Domain.Models;
+
+namespace Tests.Application.Services;
+public class OrderServiceTests
+{
+ public static TheoryData GetOrderData() =>
+ new()
+ {
+ { new() { PricePerBear = 5, Quantity = 10 }, 50 },
+ { new() { PricePerBear = 2, Quantity = 5, DiscountPercentage = .5M }, 5 },
+ { new() { PricePerBear = 100, Quantity = 100, DiscountPercentage = 1 }, 0 },
+ };
+
+ public static TheoryData GetQuotaData() =>
+ new()
+ {
+ { new() { Quantity = 0 }, 0 },
+ { new() { Quantity = 10 }, 0 },
+ { new() { Quantity = 11 }, .1M },
+ { new() { Quantity = 20 }, .1M },
+ { new() { Quantity = 22 }, .2M },
+ { new() { Quantity = 10000 }, .2M },
+ };
+
+ [Theory]
+ [MemberData(nameof(GetOrderData))]
+ public void GetsFinalPriceFromOrders(Order order, decimal expectedPrice)
+ {
+ var service = new OrderService();
+
+ var actual = service.GetFinalPrice(order);
+
+ Assert.Equal(expectedPrice, actual);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetQuotaData))]
+ public void GetsQuotaPercentageFromOrders(Order order, decimal expectedQuota)
+ {
+ var service = new OrderService();
+
+ var actual = service.GetQuotaPercentage(order);
+
+ Assert.Equal(expectedQuota, actual);
+ }
+}
diff --git a/BrewABear/Tests/Application/Services/SaleServiceTests.cs b/BrewABear/Tests/Application/Services/SaleServiceTests.cs
new file mode 100644
index 0000000..7a041c6
--- /dev/null
+++ b/BrewABear/Tests/Application/Services/SaleServiceTests.cs
@@ -0,0 +1,134 @@
+using Application.Queries.BrewerQueries;
+using Application.Services;
+using Domain.Models;
+
+namespace Tests.Application.Services;
+public class SaleServiceTests
+{
+ static List GetBeers =>
+ [
+ new() { Id = "1", Name = "Test1" },
+ new() { Id = "2", Name = "Test2" },
+ ];
+
+ static List GetWholesalers =>
+ [
+ new() { Id = "W1", Name = "Saler1" },
+ new() { Id = "W2", Name = "Saler2" },
+ ];
+
+ public static TheoryData, Beer, Wholesaler, int, List> GetExistingInventories()
+ {
+ var beers = GetBeers;
+
+ List salers = GetWholesalers;
+
+ List inventories =
+ [
+ new()
+ {
+ Beer = beers[0], BeerId = beers[0].Id,
+ Wholesaler = salers[0], WholesalerId = salers[0].Id,
+ Quantity = 5,
+ },
+ new()
+ {
+ Beer = beers[1], BeerId = beers[1].Id,
+ Wholesaler = salers[1], WholesalerId = salers[1].Id,
+ Quantity = 1,
+ },
+ ];
+
+ List quantities =
+ [
+ 1,
+ 10,
+ ];
+
+ List expectedInventories =
+ [
+ new()
+ {
+ Beer = beers[0], BeerId = beers[0].Id,
+ Wholesaler = salers[0], WholesalerId = salers[0].Id,
+ Quantity = 6,
+ },
+ new()
+ {
+ Beer = beers[1], BeerId = beers[1].Id,
+ Wholesaler = salers[1], WholesalerId = salers[1].Id,
+ Quantity = 11
+ },
+ ];
+
+ return new()
+ {
+ { [inventories[0]], beers[0], salers[0], quantities[0], [expectedInventories[0]] },
+ { [inventories[1]], beers[1], salers[1], quantities[1], [expectedInventories[1]] },
+ };
+ }
+
+ public static TheoryData, Beer, Wholesaler, int, List> GetUpdatedInventories()
+ {
+ var beers = GetBeers;
+ var salers = GetWholesalers;
+
+ List inventory =
+ [
+ new()
+ { Beer = beers[1], BeerId = beers[1].Id,
+ Wholesaler = salers[1], WholesalerId = salers[1].Id,
+ Quantity = 10
+ }
+ ];
+
+ List expectedInventory =
+ [
+ new()
+ { Beer = beers[1], BeerId = beers[1].Id,
+ Wholesaler = salers[1], WholesalerId = salers[1].Id,
+ Quantity = 10
+ }
+ ];
+
+ return new()
+ {
+ { [], beers[0], salers[0], 5, [] },
+ { inventory, beers[0], salers[0], 10, expectedInventory }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetExistingInventories))]
+ public void UpdatesExistingInventory(
+ List inventory,
+ Beer beer,
+ Wholesaler wholesaler,
+ int quantity,
+ List expected)
+ {
+ var service = new SaleService();
+
+ var result = service.CreateSale(inventory, wholesaler, beer, quantity);
+
+ Assert.Null(result);
+ Assert.Equivalent(expected, inventory);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetUpdatedInventories))]
+ public void CreatesNewInventory(
+ List inventory,
+ Beer beer,
+ Wholesaler wholesaler,
+ int quantity,
+ List expected)
+ {
+ var service = new SaleService();
+
+ var result = service.CreateSale(inventory, wholesaler, beer, quantity);
+
+ Assert.Equivalent(expected, inventory);
+ Assert.NotNull(result);
+ }
+}
diff --git a/BrewABear/Tests/Tests.csproj b/BrewABear/Tests/Tests.csproj
new file mode 100644
index 0000000..2d25cdd
--- /dev/null
+++ b/BrewABear/Tests/Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+