Merge pull request #9 from dotnet-architecture/dev
PR to merge into skynode dev branch
This commit is contained in:
commit
52e0a4e3b3
@ -115,8 +115,9 @@ services:
|
|||||||
image: redis
|
image: redis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq
|
image: rabbitmq:3-management
|
||||||
ports:
|
ports:
|
||||||
|
- "15672:15672"
|
||||||
- "5672:5672"
|
- "5672:5672"
|
@ -70,7 +70,7 @@ namespace Microsoft.Extensions.HealthChecks
|
|||||||
|
|
||||||
var properties = await tableClient.GetServicePropertiesAsync().ConfigureAwait(false);
|
var properties = await tableClient.GetServicePropertiesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (String.IsNullOrWhiteSpace(tableName))
|
if (!String.IsNullOrWhiteSpace(tableName))
|
||||||
{
|
{
|
||||||
var table = tableClient.GetTableReference(tableName);
|
var table = tableClient.GetTableReference(tableName);
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ namespace Microsoft.Extensions.HealthChecks
|
|||||||
|
|
||||||
var properties = await queueClient.GetServicePropertiesAsync().ConfigureAwait(false);
|
var properties = await queueClient.GetServicePropertiesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (String.IsNullOrWhiteSpace(queueName))
|
if (!String.IsNullOrWhiteSpace(queueName))
|
||||||
{
|
{
|
||||||
var queue = queueClient.GetQueueReference(queueName);
|
var queue = queueClient.GetQueueReference(queueName);
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ using System.Net;
|
|||||||
|
|
||||||
namespace Basket.API.Infrastructure.Filters
|
namespace Basket.API.Infrastructure.Filters
|
||||||
{
|
{
|
||||||
public class HttpGlobalExceptionFilter : IExceptionFilter
|
public partial class HttpGlobalExceptionFilter : IExceptionFilter
|
||||||
{
|
{
|
||||||
private readonly IHostingEnvironment env;
|
private readonly IHostingEnvironment env;
|
||||||
private readonly ILogger<HttpGlobalExceptionFilter> logger;
|
private readonly ILogger<HttpGlobalExceptionFilter> logger;
|
||||||
@ -52,12 +52,5 @@ namespace Basket.API.Infrastructure.Filters
|
|||||||
}
|
}
|
||||||
context.ExceptionHandled = true;
|
context.ExceptionHandled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class JsonErrorResponse
|
|
||||||
{
|
|
||||||
public string[] Messages { get; set; }
|
|
||||||
|
|
||||||
public object DeveloperMessage { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Basket.API.Infrastructure.Filters
|
||||||
|
{
|
||||||
|
public class JsonErrorResponse
|
||||||
|
{
|
||||||
|
public string[] Messages { get; set; }
|
||||||
|
|
||||||
|
public object DeveloperMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 Id { get; set; }
|
||||||
public string ProductId { get; set; }
|
public string ProductId { get; set; }
|
||||||
@ -9,5 +12,16 @@
|
|||||||
public decimal OldUnitPrice { get; set; }
|
public decimal OldUnitPrice { get; set; }
|
||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public string PictureUrl { get; set; }
|
public string PictureUrl { get; set; }
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var results = new List<ValidationResult>();
|
||||||
|
|
||||||
|
if (Quantity < 1)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult("Invalid number of units", new []{ "Quantity" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API
|
|||||||
services.AddMvc(options =>
|
services.AddMvc(options =>
|
||||||
{
|
{
|
||||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||||
|
options.Filters.Add(typeof(ValidateModelStateFilter));
|
||||||
}).AddControllersAsServices();
|
}).AddControllersAsServices();
|
||||||
|
|
||||||
services.AddHealthChecks(checks =>
|
services.AddHealthChecks(checks =>
|
||||||
|
@ -72,8 +72,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
|
|||||||
if (discount > existingOrderForProduct.GetCurrentDiscount())
|
if (discount > existingOrderForProduct.GetCurrentDiscount())
|
||||||
{
|
{
|
||||||
existingOrderForProduct.SetNewDiscount(discount);
|
existingOrderForProduct.SetNewDiscount(discount);
|
||||||
existingOrderForProduct.AddUnits(units);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingOrderForProduct.AddUnits(units);
|
||||||
}
|
}
|
||||||
else
|
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}.");
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
|
|||||||
return _units;
|
return _units;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal GetUnitPrice()
|
||||||
|
{
|
||||||
|
return _unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
public string GetOrderItemProductName() => _productName;
|
public string GetOrderItemProductName() => _productName;
|
||||||
|
|
||||||
public void SetNewDiscount(decimal discount)
|
public void SetNewDiscount(decimal discount)
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
<div class="esh-basket">
|
<div class="esh-basket">
|
||||||
<esh-header url="/catalog">Back to catalog</esh-header>
|
<esh-header url="/catalog">Back to catalog</esh-header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div *ngFor="let errorMessage of errorMessages">
|
||||||
|
<div class="esh-basket-items-margin-left1 row">
|
||||||
|
<div class="alert alert-warning" role="alert"> {{errorMessage}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<article class="esh-basket-titles row">
|
<article class="esh-basket-titles row">
|
||||||
<section class="esh-basket-title col-xs-3">Product</section>
|
<section class="esh-basket-title col-xs-3">Product</section>
|
||||||
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
||||||
@ -14,16 +20,17 @@
|
|||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
|
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
||||||
<img class="esh-basket-image" src="{{item.pictureUrl}}" />
|
<img class="esh-basket-image" src="{{item.pictureUrl}}"/>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-3">{{item.productName}}</section>
|
<section class="esh-basket-item esh-basket-item--middle col-xs-3">{{item.productName}}</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ {{item.unitPrice | number:'.2-2'}}</section>
|
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ {{item.unitPrice | number:'.2-2'}}</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
|
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
|
||||||
<input class="esh-basket-input"
|
<input id="quantity"
|
||||||
|
class="esh-basket-input"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
[(ngModel)]="item.quantity"
|
[(ngModel)]="item.quantity"
|
||||||
(change)="itemQuantityChanged(item)" />
|
(change)="itemQuantityChanged(item)"/>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ {{(item.unitPrice * item.quantity) | number:'.2-2'}}</section>
|
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ {{(item.unitPrice * item.quantity) | number:'.2-2'}}</section>
|
||||||
</article>
|
</article>
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { BasketService } from './basket.service';
|
import 'rxjs/Rx';
|
||||||
import { IBasket } from '../shared/models/basket.model';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { IBasketItem } from '../shared/models/basketItem.model';
|
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';
|
import { BasketWrapperService } from '../shared/services/basket.wrapper.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -12,6 +16,7 @@ import { BasketWrapperService } from '../shared/services/basket.wrapper.service'
|
|||||||
templateUrl: './basket.component.html'
|
templateUrl: './basket.component.html'
|
||||||
})
|
})
|
||||||
export class BasketComponent implements OnInit {
|
export class BasketComponent implements OnInit {
|
||||||
|
errorMessages: any;
|
||||||
basket: IBasket;
|
basket: IBasket;
|
||||||
totalPrice: number = 0;
|
totalPrice: number = 0;
|
||||||
|
|
||||||
@ -29,13 +34,27 @@ export class BasketComponent implements OnInit {
|
|||||||
this.service.setBasket(this.basket).subscribe(x => console.log('basket updated: ' + x));
|
this.service.setBasket(this.basket).subscribe(x => console.log('basket updated: ' + x));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(event: any) {
|
update(event: any): Observable<boolean> {
|
||||||
this.service.setBasket(this.basket).subscribe(x => console.log('basket updated: ' + x));
|
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) {
|
checkOut(event: any) {
|
||||||
this.basketwrapper.basket = this.basket;
|
this.update(event)
|
||||||
this.router.navigate(['order']);
|
.subscribe(
|
||||||
|
x => {
|
||||||
|
this.errorMessages = [];
|
||||||
|
this.basketwrapper.basket = this.basket;
|
||||||
|
this.router.navigate(['order'],
|
||||||
|
errMessage => this.errorMessages = errMessage.messages);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateTotalPrice() {
|
private calculateTotalPrice() {
|
||||||
|
@ -20,7 +20,7 @@ export class OrdersComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.configurationService.isReady) {
|
if (this.configurationService.isReady) {
|
||||||
this.getOrders()
|
this.getOrders();
|
||||||
} else {
|
} else {
|
||||||
this.configurationService.settingsLoaded$.subscribe(x => {
|
this.configurationService.settingsLoaded$.subscribe(x => {
|
||||||
this.getOrders();
|
this.getOrders();
|
||||||
@ -31,7 +31,7 @@ export class OrdersComponent implements OnInit {
|
|||||||
this.interval = setTimeout(() => {
|
this.interval = setTimeout(() => {
|
||||||
this.service.getOrders().subscribe(orders => {
|
this.service.getOrders().subscribe(orders => {
|
||||||
this.orders = orders;
|
this.orders = orders;
|
||||||
if (this.orders.length != this.oldOrders.length) {
|
if (this.orders.length !== this.oldOrders.length) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,8 @@ import { Subject } from 'rxjs/Subject';
|
|||||||
import { ICatalogItem } from '../models/catalogItem.model';
|
import { ICatalogItem } from '../models/catalogItem.model';
|
||||||
import { IBasketItem } from '../models/basketItem.model';
|
import { IBasketItem } from '../models/basketItem.model';
|
||||||
import { IBasket } from '../models/basket.model';
|
import { IBasket } from '../models/basket.model';
|
||||||
import { SecurityService } from '../services/security.service';
|
import { SecurityService } from '../services/security.service';
|
||||||
|
import { Guid } from '../../../guid';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BasketWrapperService {
|
export class BasketWrapperService {
|
||||||
@ -27,7 +28,7 @@ export class BasketWrapperService {
|
|||||||
productName: item.name,
|
productName: item.name,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unitPrice: item.price,
|
unitPrice: item.price,
|
||||||
id: '',
|
id: Guid.newGuid(),
|
||||||
oldUnitPrice: 0
|
oldUnitPrice: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ export class DataService {
|
|||||||
if (error instanceof Response) {
|
if (error instanceof Response) {
|
||||||
let errMessage = '';
|
let errMessage = '';
|
||||||
try {
|
try {
|
||||||
errMessage = error.json().error;
|
errMessage = error.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errMessage = error.statusText;
|
errMessage = error.statusText;
|
||||||
}
|
}
|
||||||
|
47
test/Services/UnitTest/Ordering/Builders.cs
Normal file
47
test/Services/UnitTest/Ordering/Builders.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
using Ordering.Domain.Events;
|
using Ordering.Domain.Events;
|
||||||
using Ordering.Domain.Exceptions;
|
using Ordering.Domain.Exceptions;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using UnitTest.Ordering;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
public class OrderAggregateTest
|
public class OrderAggregateTest
|
||||||
@ -93,6 +95,18 @@ public class OrderAggregateTest
|
|||||||
Assert.Throws<OrderingDomainException>(() => fakeOrderItem.AddUnits(-1));
|
Assert.Throws<OrderingDomainException>(() => 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]
|
[Fact]
|
||||||
public void Add_new_Order_raises_new_event()
|
public void Add_new_Order_raises_new_event()
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user