+
+
+
\ No newline at end of file
diff --git a/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.spec.ts b/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.spec.ts
new file mode 100644
index 0000000..4afa545
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.spec.ts
@@ -0,0 +1,33 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { WholesalerViewComponent } from './wholesaler-view.component';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { ActivatedRoute } from '@angular/router';
+
+describe('WholesalerViewComponent', () => {
+ let component: WholesalerViewComponent;
+ let fixture: ComponentFixture;
+
+ const mockComponent = {
+ close: jasmine.createSpy('close')
+ }
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [WholesalerViewComponent, HttpClientModule],
+ providers: [HttpClientModule, HttpClient, {
+ provide: ActivatedRoute,
+ useValue: mockComponent
+ }]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(WholesalerViewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.ts b/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.ts
new file mode 100644
index 0000000..89ac77b
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/src/app/views/wholesaler-view/wholesaler-view.component.ts
@@ -0,0 +1,73 @@
+import { Component } from '@angular/core';
+import {MatTableModule} from '@angular/material/table';
+import {MatDividerModule} from '@angular/material/divider';
+import {MatButtonModule} from '@angular/material/button';
+import { MatDialog } from '@angular/material/dialog';
+import { Router, RouterLink, RouterOutlet } from '@angular/router';
+import { StockViewComponent } from './stock-view/stock-view.component';
+import { WholesalerClass } from '../../Models/WholesalerModel';
+import { BreweryInfoService } from '../../Services/brewery-info.service';
+import { BreweryPostService } from '../../Services/brewery-post.service';
+
+@Component({
+ selector: 'app-wholesaler-view',
+ standalone: true,
+ imports: [RouterLink, RouterOutlet, MatTableModule, MatDividerModule, MatButtonModule],
+ templateUrl: './wholesaler-view.component.html',
+ styleUrl: './wholesaler-view.component.css'
+})
+export class WholesalerViewComponent {
+ wholesalers: WholesalerClass[] = [];
+ columnsToDisplay = ['id', 'name', 'stock', 'beers'];
+ clickedWholesaler!: WholesalerClass;
+
+ constructor(private breweryInfoService: BreweryInfoService, private breweryPostService: BreweryPostService, private router: Router, public dialog: MatDialog) {
+ this.getWholesalers();
+ }
+
+ deleteBrewery() {
+
+ if (this.clickedWholesaler) {
+ this.breweryPostService.deleteRow(this.clickedWholesaler.id, 'Wholesalers');
+ this.wholesalers = this.wholesalers.filter(b => b.id !== this.clickedWholesaler.id)
+ }
+ }
+
+ editItem() {
+ if (this.clickedWholesaler) {
+ let wholesaler = this.clickedWholesaler;
+ this.router.navigate([`Wholesalers/edit`], { state: {wholesaler}});
+ }
+ }
+
+ getCurrentStock(wholesaler: WholesalerClass) {
+ return wholesaler.stocks?.reduce((total, stock) => total + stock.stockQuantity, 0)
+ }
+
+ getWholesalers() {
+ this.breweryInfoService.getInfo('Wholesalers').subscribe((result) => {
+ this.wholesalers = result;
+ });
+ }
+
+ checkStock() {
+ let stocks: { beerName: any; quantity: number; }[] = [];
+
+ if (this.clickedWholesaler) {
+ this.clickedWholesaler.stocks
+ ?.forEach(stock => {
+ this.breweryInfoService
+ .getInfo('Beers', stock.beerId)
+ .subscribe(beer => {
+ stocks.push(
+ {
+ beerName: beer.name,
+ quantity: stock.stockQuantity
+ }
+ )
+ })
+ })
+ }
+ this.dialog.open(StockViewComponent, {data: stocks})
+ }
+}
diff --git a/FrontEnd/BreweryAppAngular/src/assets/.gitkeep b/FrontEnd/BreweryAppAngular/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/FrontEnd/BreweryAppAngular/src/favicon.ico b/FrontEnd/BreweryAppAngular/src/favicon.ico
new file mode 100644
index 0000000..57614f9
Binary files /dev/null and b/FrontEnd/BreweryAppAngular/src/favicon.ico differ
diff --git a/FrontEnd/BreweryAppAngular/src/index.html b/FrontEnd/BreweryAppAngular/src/index.html
new file mode 100644
index 0000000..52061d3
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ BrewApi
+
+
+
+
+
+
+
+
+
+
diff --git a/FrontEnd/BreweryAppAngular/src/main.ts b/FrontEnd/BreweryAppAngular/src/main.ts
new file mode 100644
index 0000000..35b00f3
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, appConfig)
+ .catch((err) => console.error(err));
diff --git a/FrontEnd/BreweryAppAngular/src/styles.css b/FrontEnd/BreweryAppAngular/src/styles.css
new file mode 100644
index 0000000..6daa69b
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/src/styles.css
@@ -0,0 +1,7 @@
+/* You can add global styles to this file, and also import other style files */
+
+html, body { height: 100%; }
+body { margin: 2; font-family: Roboto, "Helvetica Neue", sans-serif; }
+body {
+ background-image: url("../ZqSbrg.jpg");
+}
diff --git a/FrontEnd/BreweryAppAngular/tsconfig.app.json b/FrontEnd/BreweryAppAngular/tsconfig.app.json
new file mode 100644
index 0000000..374cc9d
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/tsconfig.app.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/FrontEnd/BreweryAppAngular/tsconfig.json b/FrontEnd/BreweryAppAngular/tsconfig.json
new file mode 100644
index 0000000..f37b67f
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/tsconfig.json
@@ -0,0 +1,33 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "lib": [
+ "ES2022",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/FrontEnd/BreweryAppAngular/tsconfig.spec.json b/FrontEnd/BreweryAppAngular/tsconfig.spec.json
new file mode 100644
index 0000000..be7e9da
--- /dev/null
+++ b/FrontEnd/BreweryAppAngular/tsconfig.spec.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..45685e0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,82 @@
+## Brewinpi
+
+Little API for the [challenge project](https://www.thecsharpacademy.com/project/64) at the C# Academy.
+
+These were the requisites
+
+- List all beers by brewery
+- A brewer can add, delete and update beers
+- Add the sale of an existing beer to an existing wholesaler
+- Upon a sale, the quantity of a beer needs to be incremented in the wholesaler's inventory
+- A client can request a quote from a wholesaler.
+- 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.
+- 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
+- A brewer brews one or several beers
+- A beer is always linked to a brewer
+- A beer can be sold by several wholesalers
+- A wholesaler sells a defined list of beers, from any brewer, and has only a limited stock of those beers
+- The beers sold by the wholesaler have a fixed price imposed by the brewery
+- For this assessment, it is considered that all sales are made without tax
+- The database is pre-filled by you
+- No front-end is needed, just the API
+- Use REST architecture
+- Use Entity Framework
+- No migrations are needed; use Ensure Deleted and Ensure Created to facilitate development and code reviews.
+
+All requisites have been taken care off, the challenge requisites were also all completed. Those were:
+
+- Add unit tests to make sure business constraints are accurate.
+- Include a Read me with your thought process, your challenges and instructions on how to run the app.
+- 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.
+- Create a separate project with a front-end of your choice. Provide instructions on how to run it.
+
+### The Api
+
+#### BackEnd and BackEnd Tests
+
+1. Clone the project, open the solution with visual studio and run the application.
+2. You can also run the tests by right clicking the solution and choosing "run tests"
+
+#### FrontEnd and FrontEnd Tests
+
+1. Clone the project, go to *FrontEnd\BreweryAppAngular* and run "ng serve"
+2. For the jasmine tests, go to the same folder and run "ng test"
+
+
+### Challenges and what was learned
+
+This project was very fun to do!
+
+I feel the back end was very straightforward to do but i definetly slowed down when i got to the front end part. Its interesting but i don't really like doing all the setup of pages and CSS.
+That said it was refreshing re-learning angular with their new update that came out a few weeks ago, so taking this project at this time was probably beneficial.
+I will probably stick with angular for most thigns and just learn the necessary for react.
+
+Will still do projects with it but typescript and the way angular organizes things is really good,
+especially with the new update where you don't need to have the central file for all your imports and now everything is component based.
+
+I tried to make the project use the repository pattern but i didn't really felt the need to add the service classes for the controllers at first, but when i was refactoring the tests and some of the controllers to have the functionality i need i realized why they are so necessary.
+My controllers started getting bloated and was necessary to separate the functionality of validation and inserting.
+
+There was also some difficulty regarding the new way .NET API projects work, the new way is using the program.cs file instead of the old startup.cs file, so a lot of ways to add middleware and how to make the system recognize your injections took some googling.
+
+Figuring out how to organize the database took some time too but i feel i did a good job with it.
+I also had to use DTO's to send especialized data to the API/frontEnd but i'm not sure they were used correctly or if my implementation is the best practice, definetly something that would probably begood to have someone more experienced to take a look at.
+
+#### Tests
+
+The main challenges i feel were actually writing tests? I feel that test documentation is something that is sorely lacking overall. Test documentantions and examples only do simple examples like "testing a calculator"
+and most courses or tuorials for the language gloss over that topic a LOT. I believe tests and knolowdge about tests are something that everyone looks for in projects and new hires but the material is really lacking on that front.
+
+I did write tests to what i feel was encessary, but i'll definetly need to keep improving on that front for future projects.
+
+
+#### Main takeaways
+
+Overall i feel this project was a good first challenge to do and a way to put some of my C# knowlowdge to the test.
+I definetly feel i improved but there's still somethings i would like to work on more like delegates, didn't use that a lot here.
+LINQ was very fun to use along with EF but i could definetly go deeper with both of these things.
+
+I worked with angular before so most of it was familiar, the new stuff definetly made it a better experience to use than before.
+Testing took a while to grasp, but when i understood the framework better it was interesting to write some tests. Definetly something i also need to work a lot on though.
+
+Tahnks for reading!
\ No newline at end of file
diff --git a/Tests/BreweryTests/BreweryTests.csproj b/Tests/BreweryTests/BreweryTests.csproj
new file mode 100644
index 0000000..05aa9ef
--- /dev/null
+++ b/Tests/BreweryTests/BreweryTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/Tests/BreweryTests/DatabaseControllerTests.cs b/Tests/BreweryTests/DatabaseControllerTests.cs
new file mode 100644
index 0000000..0fe32ba
--- /dev/null
+++ b/Tests/BreweryTests/DatabaseControllerTests.cs
@@ -0,0 +1,176 @@
+using BreweryApi.Controllers;
+using BreweryApi.Models;
+using BreweryApi.Repositories;
+using BreweryApi.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace BreweryTests
+{
+ [Collection("ControllerCollection")]
+ public class DatabaseControllerTests : IDisposable
+ {
+ private readonly BreweryContext _dbContext;
+
+ private readonly WholesalerRepository _wholesalerRepository;
+ private readonly BeerRepository _beerRepository;
+ private readonly SalesRepository _salesRepository;
+ private readonly BreweryRepository _breweryRepository;
+ private readonly WholesalerStockRepository _wholesalerStockRepository;
+
+ private readonly SalesService _salesService;
+ private readonly WholesalerService _wholesalerService;
+
+ private readonly SalesController _saleController;
+ private readonly WholesalersController _wholesalerController;
+
+ public DatabaseControllerTests()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BrewTesting;Trusted_Connection=True;MultipleActiveResultSets=true")
+ .Options;
+
+ _dbContext = new BreweryContext(options);
+
+ _wholesalerStockRepository = new WholesalerStockRepository(_dbContext);
+ _wholesalerRepository = new WholesalerRepository(_dbContext);
+ _beerRepository = new BeerRepository(_dbContext);
+ _salesRepository = new SalesRepository(_dbContext);
+ _breweryRepository = new BreweryRepository(_dbContext);
+
+ _salesService = new SalesService(_salesRepository, _beerRepository, _breweryRepository, _wholesalerRepository);
+ _wholesalerService = new WholesalerService(_wholesalerRepository, _salesRepository, _beerRepository, _wholesalerStockRepository);
+
+ _saleController = new SalesController(_salesService);
+ _wholesalerController = new WholesalersController(_wholesalerService);
+
+ _dbContext.Database.EnsureDeleted();
+ _dbContext.Database.EnsureCreated();
+ }
+
+ public void Dispose()
+ {
+ _dbContext.Dispose();
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_WholesalerNotBuyingAssignedBeer()
+ {
+
+ var sale = new Sales { BeerId = 2, BreweryId = 2, Quantity = 1, SaleDate = new DateTime(), WholeSalerId = 3 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_WholesalerBuyingOverStock()
+ {
+
+ var sale = new Sales { BeerId = 1, BreweryId = 1, Quantity = 100000, SaleDate = new DateTime(), WholeSalerId = 1 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_WholesalerDontExist()
+ {
+
+ var sale = new Sales { BeerId = 1, BreweryId = 1, Quantity = 1, SaleDate = new DateTime(), WholeSalerId = 5 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_BeerDontExist()
+ {
+
+ var sale = new Sales { BeerId = 99, BreweryId = 1, Quantity = 1, SaleDate = new DateTime(), WholeSalerId = 1 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_BreweryDontExist()
+ {
+
+ var sale = new Sales { BeerId = 1, BreweryId = 99, Quantity = 1, SaleDate = new DateTime(), WholeSalerId = 1 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_SaleWith0OrNoQuantity()
+ {
+
+ var sale = new Sales { BeerId = 1, BreweryId = 1, Quantity = 0, SaleDate = new DateTime(), WholeSalerId = 1 };
+
+ await _saleController.PostSales(sale);
+
+ var updatedEntity = _dbContext.Sales.FirstOrDefault(e => e.Id == 5);
+
+ Assert.Null(updatedEntity);
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_WholesalerDosntExist()
+ {
+
+ ActionResult quoteResult = await _wholesalerController.GetQuote(99, 1, 100);
+ BadRequestObjectResult q = (BadRequestObjectResult)quoteResult.Result;
+
+ Assert.True(q.Value == "Beer or wholesaler don't exist");
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_BeerDosntExist()
+ {
+
+ ActionResult quoteResult = await _wholesalerController.GetQuote(1, 99, 100);
+ BadRequestObjectResult q = (BadRequestObjectResult)quoteResult.Result;
+
+ Assert.True(q.Value == "Beer or wholesaler don't exist");
+ }
+
+ [Fact]
+ public async Task TestCase_Fail_If_WholesalerDosntSellBeer()
+ {
+
+ ActionResult quoteResult = await _wholesalerController.GetQuote(1, 3, 100);
+ BadRequestObjectResult q = (BadRequestObjectResult)quoteResult.Result;
+
+ Assert.True(q.Value == "Wholesaler can't sell this beer");
+ }
+
+ [Fact]
+ public async Task TestCase_Pass_If_QuoteValid()
+ {
+
+ var quoteResult = await _wholesalerController.GetQuote(1, 1, 100);
+ ContentResult quote = (ContentResult)quoteResult.Result;
+
+ string valid = "\"The price for the quoted order from Beer Dreams for 100 units of Malt will total at around 1500,00\"";
+
+ Assert.True(quote.Content == valid);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/BreweryTests/GlobalUsings.cs b/Tests/BreweryTests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/Tests/BreweryTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file