diff --git a/docker-compose.yml b/docker-compose.yml index 253674b57..86931228e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,8 +115,9 @@ services: image: redis ports: - "6379:6379" - + rabbitmq: - image: rabbitmq + image: rabbitmq:3-management ports: + - "15672:15672" - "5672:5672" \ No newline at end of file diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.AzureStorage/AzureHealthCheckBuilderExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.AzureStorage/AzureHealthCheckBuilderExtensions.cs index 4c917f223..5a06a3ba2 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.AzureStorage/AzureHealthCheckBuilderExtensions.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.AzureStorage/AzureHealthCheckBuilderExtensions.cs @@ -70,7 +70,7 @@ namespace Microsoft.Extensions.HealthChecks var properties = await tableClient.GetServicePropertiesAsync().ConfigureAwait(false); - if (String.IsNullOrWhiteSpace(tableName)) + if (!String.IsNullOrWhiteSpace(tableName)) { var table = tableClient.GetTableReference(tableName); @@ -150,7 +150,7 @@ namespace Microsoft.Extensions.HealthChecks var properties = await queueClient.GetServicePropertiesAsync().ConfigureAwait(false); - if (String.IsNullOrWhiteSpace(queueName)) + if (!String.IsNullOrWhiteSpace(queueName)) { var queue = queueClient.GetQueueReference(queueName); diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index ab7989973..dab725915 100644 --- a/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -8,7 +8,7 @@ using System.Net; namespace Basket.API.Infrastructure.Filters { - public class HttpGlobalExceptionFilter : IExceptionFilter + public partial class HttpGlobalExceptionFilter : IExceptionFilter { private readonly IHostingEnvironment env; private readonly ILogger logger; @@ -52,12 +52,5 @@ namespace Basket.API.Infrastructure.Filters } context.ExceptionHandled = true; } - - private class JsonErrorResponse - { - public string[] Messages { get; set; } - - public object DeveloperMessage { get; set; } - } } } diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs new file mode 100644 index 000000000..bcadc7358 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Basket.API.Infrastructure.Filters +{ + public class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } +} diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs new file mode 100644 index 000000000..8ef72edb6 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Basket.API.Infrastructure.Filters +{ + public class ValidateModelStateFilter : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.ModelState.IsValid) + { + return; + } + + var validationErrors = context.ModelState + .Keys + .SelectMany(k => context.ModelState[k].Errors) + .Select(e => e.ErrorMessage) + .ToArray(); + + var json = new JsonErrorResponse + { + Messages = validationErrors + }; + + context.Result = new BadRequestObjectResult(json); + } + } +} diff --git a/src/Services/Basket/Basket.API/Model/BasketItem.cs b/src/Services/Basket/Basket.API/Model/BasketItem.cs index 8b98befcc..a41945145 100644 --- a/src/Services/Basket/Basket.API/Model/BasketItem.cs +++ b/src/Services/Basket/Basket.API/Model/BasketItem.cs @@ -1,6 +1,9 @@ -namespace Microsoft.eShopOnContainers.Services.Basket.API.Model +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopOnContainers.Services.Basket.API.Model { - public class BasketItem + public class BasketItem : IValidatableObject { public string Id { get; set; } public string ProductId { get; set; } @@ -9,5 +12,16 @@ public decimal OldUnitPrice { get; set; } public int Quantity { get; set; } public string PictureUrl { get; set; } + public IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + + if (Quantity < 1) + { + results.Add(new ValidationResult("Invalid number of units", new []{ "Quantity" })); + } + + return results; + } } } diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index a473a29fc..18e21e209 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -54,6 +54,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddMvc(options => { options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + options.Filters.Add(typeof(ValidateModelStateFilter)); }).AddControllersAsServices(); services.AddHealthChecks(checks => diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 37c17c897..ae2194948 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -72,8 +72,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O if (discount > existingOrderForProduct.GetCurrentDiscount()) { existingOrderForProduct.SetNewDiscount(discount); - existingOrderForProduct.AddUnits(units); } + + existingOrderForProduct.AddUnits(units); } else { @@ -187,6 +188,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O { throw new OrderingDomainException($"Not possible to change order status from {OrderStatus.Name} to {orderStatusToChange.Name}."); } + + public decimal GetTotal() + { + return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice()); + } } } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs index fd65fb3ca..1b7896dcc 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs @@ -59,6 +59,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O return _units; } + public decimal GetUnitPrice() + { + return _unitPrice; + } + public string GetOrderItemProductName() => _productName; public void SetNewDiscount(decimal discount) diff --git a/src/Web/WebSPA/Client/modules/basket/basket.component.html b/src/Web/WebSPA/Client/modules/basket/basket.component.html index 6787bc0cb..52aab2fe3 100644 --- a/src/Web/WebSPA/Client/modules/basket/basket.component.html +++ b/src/Web/WebSPA/Client/modules/basket/basket.component.html @@ -1,7 +1,13 @@
Back to catalog - +
+
+
+ +
+
+
Product
@@ -14,16 +20,17 @@
- +
{{item.productName}}
$ {{item.unitPrice | number:'.2-2'}}
- + (change)="itemQuantityChanged(item)"/>
$ {{(item.unitPrice * item.quantity) | number:'.2-2'}}
diff --git a/src/Web/WebSPA/Client/modules/basket/basket.component.ts b/src/Web/WebSPA/Client/modules/basket/basket.component.ts index 4637e93fa..b60731e91 100644 --- a/src/Web/WebSPA/Client/modules/basket/basket.component.ts +++ b/src/Web/WebSPA/Client/modules/basket/basket.component.ts @@ -1,9 +1,13 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; -import { BasketService } from './basket.service'; -import { IBasket } from '../shared/models/basket.model'; -import { IBasketItem } from '../shared/models/basketItem.model'; +import 'rxjs/Rx'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; + +import { BasketService } from './basket.service'; +import { IBasket } from '../shared/models/basket.model'; +import { IBasketItem } from '../shared/models/basketItem.model'; import { BasketWrapperService } from '../shared/services/basket.wrapper.service'; @Component({ @@ -12,6 +16,7 @@ import { BasketWrapperService } from '../shared/services/basket.wrapper.service' templateUrl: './basket.component.html' }) export class BasketComponent implements OnInit { + errorMessages: any; basket: IBasket; totalPrice: number = 0; @@ -29,13 +34,27 @@ export class BasketComponent implements OnInit { this.service.setBasket(this.basket).subscribe(x => console.log('basket updated: ' + x)); } - update(event: any) { - this.service.setBasket(this.basket).subscribe(x => console.log('basket updated: ' + x)); + update(event: any): Observable { + let setBasketObservable = this.service.setBasket(this.basket); + setBasketObservable + .subscribe( + x => { + this.errorMessages = []; + console.log('basket updated: ' + x); + }, + errMessage => this.errorMessages = errMessage.messages); + return setBasketObservable; } checkOut(event: any) { - this.basketwrapper.basket = this.basket; - this.router.navigate(['order']); + this.update(event) + .subscribe( + x => { + this.errorMessages = []; + this.basketwrapper.basket = this.basket; + this.router.navigate(['order'], + errMessage => this.errorMessages = errMessage.messages); + }); } private calculateTotalPrice() { diff --git a/src/Web/WebSPA/Client/modules/orders/orders.component.ts b/src/Web/WebSPA/Client/modules/orders/orders.component.ts index 105965200..a6af462ac 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders.component.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders.component.ts @@ -20,7 +20,7 @@ export class OrdersComponent implements OnInit { ngOnInit() { if (this.configurationService.isReady) { - this.getOrders() + this.getOrders(); } else { this.configurationService.settingsLoaded$.subscribe(x => { this.getOrders(); @@ -31,7 +31,7 @@ export class OrdersComponent implements OnInit { this.interval = setTimeout(() => { this.service.getOrders().subscribe(orders => { this.orders = orders; - if (this.orders.length != this.oldOrders.length) { + if (this.orders.length !== this.oldOrders.length) { clearInterval(this.interval); } }); diff --git a/src/Web/WebSPA/Client/modules/shared/services/basket.wrapper.service.ts b/src/Web/WebSPA/Client/modules/shared/services/basket.wrapper.service.ts index 3017a30f3..447fd9441 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/basket.wrapper.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/basket.wrapper.service.ts @@ -4,7 +4,8 @@ import { Subject } from 'rxjs/Subject'; import { ICatalogItem } from '../models/catalogItem.model'; import { IBasketItem } from '../models/basketItem.model'; import { IBasket } from '../models/basket.model'; -import { SecurityService } from '../services/security.service'; +import { SecurityService } from '../services/security.service'; +import { Guid } from '../../../guid'; @Injectable() export class BasketWrapperService { @@ -27,7 +28,7 @@ export class BasketWrapperService { productName: item.name, quantity: 1, unitPrice: item.price, - id: '', + id: Guid.newGuid(), oldUnitPrice: 0 }; diff --git a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts index 64288e127..26f7801ca 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts @@ -102,7 +102,7 @@ export class DataService { if (error instanceof Response) { let errMessage = ''; try { - errMessage = error.json().error; + errMessage = error.json(); } catch (err) { errMessage = error.statusText; } diff --git a/test/Services/UnitTest/Ordering/Builders.cs b/test/Services/UnitTest/Ordering/Builders.cs new file mode 100644 index 000000000..c9dd9d366 --- /dev/null +++ b/test/Services/UnitTest/Ordering/Builders.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + +namespace UnitTest.Ordering +{ + public class AddressBuilder + { + public Address Build() + { + return new Address("street", "city", "state", "country", "zipcode"); + } + } + + public class OrderBuilder + { + private readonly Order order; + + public OrderBuilder(Address address) + { + order = new Order( + "userId", + address, + cardTypeId:5, + cardNumber:"12", + cardSecurityNumber:"123", + cardHolderName:"name", + cardExpiration:DateTime.UtcNow); + } + + public OrderBuilder AddOne( + int productId, + string productName, + decimal unitPrice, + decimal discount, + string pictureUrl, + int units = 1) + { + order.AddOrderItem(productId, productName, unitPrice, discount, pictureUrl, units); + return this; + } + + public Order Build() + { + return order; + } + } +} diff --git a/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs b/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs index dfa31eb62..eaa0d0cb6 100644 --- a/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs +++ b/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs @@ -2,6 +2,8 @@ using Ordering.Domain.Events; using Ordering.Domain.Exceptions; using System; +using System.Linq; +using UnitTest.Ordering; using Xunit; public class OrderAggregateTest @@ -93,6 +95,18 @@ public class OrderAggregateTest Assert.Throws(() => fakeOrderItem.AddUnits(-1)); } + [Fact] + public void when_add_two_times_on_the_same_item_then_the_total_of_order_should_be_the_sum_of_the_two_items() + { + var address = new AddressBuilder().Build(); + var order = new OrderBuilder(address) + .AddOne(1,"cup",10.0m,0,string.Empty) + .AddOne(1,"cup",10.0m,0,string.Empty) + .Build(); + + Assert.Equal(20.0m, order.GetTotal()); + } + [Fact] public void Add_new_Order_raises_new_event() {