Add coupon support to api services
This commit is contained in:
parent
e80e1b0d0e
commit
86cedb4bcf
@ -72,6 +72,19 @@ static_resources:
|
|||||||
route:
|
route:
|
||||||
auto_host_rewrite: true
|
auto_host_rewrite: true
|
||||||
cluster: basket
|
cluster: basket
|
||||||
|
- name: "cp-short"
|
||||||
|
match:
|
||||||
|
prefix: "/cp/"
|
||||||
|
route:
|
||||||
|
auto_host_rewrite: true
|
||||||
|
prefix_rewrite: "/coupon-api/"
|
||||||
|
cluster: coupon
|
||||||
|
- name: "cp-long"
|
||||||
|
match:
|
||||||
|
prefix: "/coupon-api/"
|
||||||
|
route:
|
||||||
|
auto_host_rewrite: true
|
||||||
|
cluster: coupon
|
||||||
- name: "agg"
|
- name: "agg"
|
||||||
match:
|
match:
|
||||||
prefix: "/"
|
prefix: "/"
|
||||||
@ -140,3 +153,11 @@ static_resources:
|
|||||||
- socket_address:
|
- socket_address:
|
||||||
address: ordering-signalrhub
|
address: ordering-signalrhub
|
||||||
port_value: 80
|
port_value: 80
|
||||||
|
- name: coupon
|
||||||
|
connect_timeout: 0.25s
|
||||||
|
type: strict_dns
|
||||||
|
lb_policy: round_robin
|
||||||
|
hosts:
|
||||||
|
- socket_address:
|
||||||
|
address: coupon-api
|
||||||
|
port_value: 80
|
@ -37,5 +37,7 @@ public class UrlsConfig
|
|||||||
public string GrpcCatalog { get; set; }
|
public string GrpcCatalog { get; set; }
|
||||||
|
|
||||||
public string GrpcOrdering { get; set; }
|
public string GrpcOrdering { get; set; }
|
||||||
|
|
||||||
|
public string Coupon { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
|
||||||
|
|
||||||
[Route("api/v1/[controller]")]
|
[Route("api/v1/[controller]")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@ -7,11 +9,13 @@ public class BasketController : ControllerBase
|
|||||||
{
|
{
|
||||||
private readonly ICatalogService _catalog;
|
private readonly ICatalogService _catalog;
|
||||||
private readonly IBasketService _basket;
|
private readonly IBasketService _basket;
|
||||||
|
private readonly ICouponService _coupon;
|
||||||
|
|
||||||
public BasketController(ICatalogService catalogService, IBasketService basketService)
|
public BasketController(ICatalogService catalogService, IBasketService basketService, ICouponService couponService)
|
||||||
{
|
{
|
||||||
_catalog = catalogService;
|
_catalog = catalogService;
|
||||||
_basket = basketService;
|
_basket = basketService;
|
||||||
|
_coupon = couponService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -151,4 +155,24 @@ public class BasketController : ControllerBase
|
|||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("coupon/{code}")]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||||
|
[ProducesResponseType(typeof(CouponData), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<ActionResult<CouponData>> CheckCouponAsync(string code)
|
||||||
|
{
|
||||||
|
var response = await _coupon.CheckCouponByCodeNumberAsync(code);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var couponResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var data = JsonConvert.DeserializeObject<CouponData>(couponResponse);
|
||||||
|
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
||||||
|
public class CouponData
|
||||||
|
{
|
||||||
|
public int Discount { get; set; }
|
||||||
|
|
||||||
|
public string Code { get; set; }
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
|
|
||||||
|
public class CouponService : ICouponService
|
||||||
|
{
|
||||||
|
public readonly HttpClient _httpClient;
|
||||||
|
private readonly UrlsConfig _urls;
|
||||||
|
private readonly ILogger<CouponService> _logger;
|
||||||
|
|
||||||
|
public CouponService(HttpClient httpClient, IOptions<UrlsConfig> config, ILogger<CouponService> logger)
|
||||||
|
{
|
||||||
|
_urls = config.Value;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> CheckCouponByCodeNumberAsync(string codeNumber)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Call coupon api with codenumber: {codeNumber}", codeNumber);
|
||||||
|
|
||||||
|
var url = new Uri($"{_urls.Catalog}/api/v1/coupon/{codeNumber}");
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync(url);
|
||||||
|
|
||||||
|
_logger.LogInformation("Coupon api response: {@response}", response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
|
|
||||||
|
public interface ICouponService
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> CheckCouponByCodeNumberAsync(string codeNumber);
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
|
||||||
|
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
@ -12,7 +14,7 @@ public class Startup
|
|||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHealthChecks()
|
var healthCheckBuilder = services.AddHealthChecks()
|
||||||
.AddCheck("self", () => HealthCheckResult.Healthy())
|
.AddCheck("self", () => HealthCheckResult.Healthy())
|
||||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
||||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
||||||
@ -20,6 +22,13 @@ public class Startup
|
|||||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
|
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
|
||||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
|
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
|
||||||
|
|
||||||
|
// HC dependency is configured this way to save one build step on ACR
|
||||||
|
var couponHealthEndpoint = Configuration["CouponUrlHC"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(couponHealthEndpoint))
|
||||||
|
{
|
||||||
|
healthCheckBuilder.AddUrlGroup(new Uri(couponHealthEndpoint), name: "couponapi-check", tags: new string[] { "couponapi" });
|
||||||
|
}
|
||||||
|
|
||||||
services.AddCustomMvc(Configuration)
|
services.AddCustomMvc(Configuration)
|
||||||
.AddCustomAuthentication(Configuration)
|
.AddCustomAuthentication(Configuration)
|
||||||
.AddDevspaces()
|
.AddDevspaces()
|
||||||
@ -163,6 +172,9 @@ public static class ServiceCollectionExtensions
|
|||||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
|
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
|
||||||
.AddDevspacesSupport();
|
.AddDevspacesSupport();
|
||||||
|
|
||||||
|
services.AddHttpClient<ICouponService, CouponService>()
|
||||||
|
. AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,9 +58,24 @@ public class BasketController : ControllerBase
|
|||||||
|
|
||||||
var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value;
|
var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value;
|
||||||
|
|
||||||
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
|
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(
|
||||||
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
|
userId,
|
||||||
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket);
|
userName,
|
||||||
|
basketCheckout.City,
|
||||||
|
basketCheckout.Street,
|
||||||
|
basketCheckout.State,
|
||||||
|
basketCheckout.Country,
|
||||||
|
basketCheckout.ZipCode,
|
||||||
|
basketCheckout.CardNumber,
|
||||||
|
basketCheckout.CardHolderName,
|
||||||
|
basketCheckout.CardExpiration,
|
||||||
|
basketCheckout.CardSecurityNumber,
|
||||||
|
basketCheckout.CardTypeId,
|
||||||
|
basketCheckout.Buyer,
|
||||||
|
basketCheckout.RequestId,
|
||||||
|
basket,
|
||||||
|
basketCheckout.Coupon,
|
||||||
|
basketCheckout.Discount);
|
||||||
|
|
||||||
// Once basket is checkout, sends an integration event to
|
// Once basket is checkout, sends an integration event to
|
||||||
// ordering.api to convert basket to order and proceeds with
|
// ordering.api to convert basket to order and proceeds with
|
||||||
|
@ -34,10 +34,16 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
|||||||
|
|
||||||
public CustomerBasket Basket { get; }
|
public CustomerBasket Basket { get; }
|
||||||
|
|
||||||
|
public string CodeDiscount { get; set; }
|
||||||
|
|
||||||
|
public int Discount { get; set; }
|
||||||
|
|
||||||
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
||||||
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
||||||
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
||||||
CustomerBasket basket)
|
CustomerBasket basket,
|
||||||
|
string codeDiscount,
|
||||||
|
int discount)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
UserName = userName;
|
UserName = userName;
|
||||||
@ -54,6 +60,8 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
|||||||
Buyer = buyer;
|
Buyer = buyer;
|
||||||
Basket = basket;
|
Basket = basket;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
|
CodeDiscount = codeDiscount;
|
||||||
|
Discount = discount;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,4 +25,8 @@ public class BasketCheckout
|
|||||||
public string Buyer { get; set; }
|
public string Buyer { get; set; }
|
||||||
|
|
||||||
public Guid RequestId { get; set; }
|
public Guid RequestId { get; set; }
|
||||||
|
|
||||||
|
public string Coupon { get; set; }
|
||||||
|
|
||||||
|
public int Discount { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
new ApiResource("webshoppingagg", "Web Shopping Aggregator"),
|
new ApiResource("webshoppingagg", "Web Shopping Aggregator"),
|
||||||
new ApiResource("orders.signalrhub", "Ordering Signalr Hub"),
|
new ApiResource("orders.signalrhub", "Ordering Signalr Hub"),
|
||||||
new ApiResource("webhooks", "Webhooks registration Service"),
|
new ApiResource("webhooks", "Webhooks registration Service"),
|
||||||
|
new ApiResource("coupon", "Coupon Service"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +54,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
"basket",
|
"basket",
|
||||||
"webshoppingagg",
|
"webshoppingagg",
|
||||||
"orders.signalrhub",
|
"orders.signalrhub",
|
||||||
"webhooks"
|
"webhooks",
|
||||||
|
"coupon"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new Client
|
new Client
|
||||||
@ -79,7 +81,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
"orders",
|
"orders",
|
||||||
"basket",
|
"basket",
|
||||||
"mobileshoppingagg",
|
"mobileshoppingagg",
|
||||||
"webhooks"
|
"webhooks",
|
||||||
|
"coupon"
|
||||||
},
|
},
|
||||||
//Allow requesting refresh tokens for long lived API access
|
//Allow requesting refresh tokens for long lived API access
|
||||||
AllowOfflineAccess = true,
|
AllowOfflineAccess = true,
|
||||||
@ -117,7 +120,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
"basket",
|
"basket",
|
||||||
"webshoppingagg",
|
"webshoppingagg",
|
||||||
"orders.signalrhub",
|
"orders.signalrhub",
|
||||||
"webhooks"
|
"webhooks",
|
||||||
|
"coupon"
|
||||||
},
|
},
|
||||||
AccessTokenLifetime = 60*60*2, // 2 hours
|
AccessTokenLifetime = 60*60*2, // 2 hours
|
||||||
IdentityTokenLifetime= 60*60*2 // 2 hours
|
IdentityTokenLifetime= 60*60*2 // 2 hours
|
||||||
@ -183,7 +187,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
"orders",
|
"orders",
|
||||||
"basket",
|
"basket",
|
||||||
"webshoppingagg",
|
"webshoppingagg",
|
||||||
"webhooks"
|
"webhooks",
|
||||||
|
"coupon"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new Client
|
new Client
|
||||||
@ -261,6 +266,21 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
|
|||||||
{
|
{
|
||||||
"webhooks"
|
"webhooks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
new Client
|
||||||
|
{
|
||||||
|
ClientId = "couponswaggerui",
|
||||||
|
ClientName = "Coupon Swagger UI",
|
||||||
|
AllowedGrantTypes = GrantTypes.Implicit,
|
||||||
|
AllowAccessTokensViaBrowser = true,
|
||||||
|
|
||||||
|
RedirectUris = { $"{clientsUrl["CouponApi"]}/swagger/oauth2-redirect.html" },
|
||||||
|
PostLogoutRedirectUris = { $"{clientsUrl["CouponApi"]}/swagger/" },
|
||||||
|
|
||||||
|
AllowedScopes =
|
||||||
|
{
|
||||||
|
"coupon"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Data
|
|||||||
clientUrls.Add("WebShoppingAgg", configuration.GetValue<string>("WebShoppingAggClient"));
|
clientUrls.Add("WebShoppingAgg", configuration.GetValue<string>("WebShoppingAggClient"));
|
||||||
clientUrls.Add("WebhooksApi", configuration.GetValue<string>("WebhooksApiClient"));
|
clientUrls.Add("WebhooksApi", configuration.GetValue<string>("WebhooksApiClient"));
|
||||||
clientUrls.Add("WebhooksWeb", configuration.GetValue<string>("WebhooksWebClient"));
|
clientUrls.Add("WebhooksWeb", configuration.GetValue<string>("WebhooksWebClient"));
|
||||||
|
clientUrls.Add("CouponApi", configuration.GetValue<string>("CouponApiClient"));
|
||||||
|
|
||||||
if (!context.Clients.Any())
|
if (!context.Clients.Any())
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
namespace Ordering.API.Application.Commands;
|
||||||
|
|
||||||
|
public class CouponConfirmedCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int OrderNumber { get; private set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int Discount { get; private set; }
|
||||||
|
|
||||||
|
public CouponConfirmedCommand(int orderNumber, int discount)
|
||||||
|
{
|
||||||
|
OrderNumber = orderNumber;
|
||||||
|
Discount = discount;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
namespace Ordering.API.Application.Commands;
|
||||||
|
|
||||||
|
public class CouponConfirmedCommandHandler : IRequestHandler<CouponConfirmedCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
|
||||||
|
public CouponConfirmedCommandHandler(IOrderRepository orderRepository)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Handle(CouponConfirmedCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
|
||||||
|
|
||||||
|
if (orderToUpdate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderToUpdate.ProcessCouponConfirmed();
|
||||||
|
|
||||||
|
return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CouponConfirmIdenfifiedCommandHandler : IdentifiedCommandHandler<CouponConfirmedCommand, bool>
|
||||||
|
{
|
||||||
|
public CouponConfirmIdenfifiedCommandHandler(
|
||||||
|
IMediator mediator,
|
||||||
|
IRequestManager requestManager,
|
||||||
|
ILogger<IdentifiedCommandHandler<CouponConfirmedCommand, bool>> logger)
|
||||||
|
: base(mediator, requestManager, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateResultForDuplicateRequest()
|
||||||
|
{
|
||||||
|
return true; // Ignore duplicate requests for processing order.
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,12 @@ public class CreateOrderCommand
|
|||||||
[DataMember]
|
[DataMember]
|
||||||
public int CardTypeId { get; private set; }
|
public int CardTypeId { get; private set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public string CodeDiscount { get; private set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public decimal Discount { get; private set; }
|
||||||
|
|
||||||
[DataMember]
|
[DataMember]
|
||||||
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
|
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
|
||||||
|
|
||||||
@ -64,7 +70,7 @@ public class CreateOrderCommand
|
|||||||
|
|
||||||
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
|
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
|
||||||
string cardNumber, string cardHolderName, DateTime cardExpiration,
|
string cardNumber, string cardHolderName, DateTime cardExpiration,
|
||||||
string cardSecurityNumber, int cardTypeId) : this()
|
string cardSecurityNumber, int cardTypeId, string codeDiscount, decimal discount) : this()
|
||||||
{
|
{
|
||||||
_orderItems = basketItems.ToOrderItemsDTO().ToList();
|
_orderItems = basketItems.ToOrderItemsDTO().ToList();
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
@ -79,6 +85,8 @@ public class CreateOrderCommand
|
|||||||
CardExpiration = cardExpiration;
|
CardExpiration = cardExpiration;
|
||||||
CardSecurityNumber = cardSecurityNumber;
|
CardSecurityNumber = cardSecurityNumber;
|
||||||
CardTypeId = cardTypeId;
|
CardTypeId = cardTypeId;
|
||||||
|
CodeDiscount = codeDiscount;
|
||||||
|
Discount = discount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ public class CreateOrderCommandHandler
|
|||||||
// methods and constructor so validations, invariants and business logic
|
// methods and constructor so validations, invariants and business logic
|
||||||
// make sure that consistency is preserved across the whole aggregate
|
// make sure that consistency is preserved across the whole aggregate
|
||||||
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
|
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
|
||||||
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
|
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration, "", 0);
|
||||||
|
|
||||||
foreach (var item in message.OrderItems)
|
foreach (var item in message.OrderItems)
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ public class OrderCancelledDomainEventHandler
|
|||||||
var order = await _orderRepository.GetAsync(orderCancelledDomainEvent.Order.Id);
|
var order = await _orderRepository.GetAsync(orderCancelledDomainEvent.Order.Id);
|
||||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||||
|
|
||||||
var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name, order.DiscountCode);
|
||||||
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToCancelledIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToCancelledIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
using Ordering.Domain.Events;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.DomainEventHandlers.OrderCoupon;
|
||||||
|
|
||||||
|
public class OrderStatusChangedToAwaitingCouponValidationDomainEventHandler : INotificationHandler<OrderStatusChangedToAwaitingCouponValidationDomainEvent>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
private readonly IBuyerRepository _buyerRepository;
|
||||||
|
private readonly ILoggerFactory _logger;
|
||||||
|
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||||
|
|
||||||
|
public OrderStatusChangedToAwaitingCouponValidationDomainEventHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
IBuyerRepository buyerRepository,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
IOrderingIntegrationEventService orderingIntegrationEventService)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||||
|
_buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_orderingIntegrationEventService = orderingIntegrationEventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(OrderStatusChangedToAwaitingCouponValidationDomainEvent domainEvent, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.CreateLogger<OrderStatusChangedToAwaitingCouponValidationDomainEventHandler>()
|
||||||
|
.LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", domainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id);
|
||||||
|
|
||||||
|
var order = await _orderRepository.GetAsync(domainEvent.OrderId);
|
||||||
|
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||||
|
|
||||||
|
var integrationEvent = new OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name, order.DiscountCode);
|
||||||
|
|
||||||
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
using Ordering.API.Application.Commands;
|
||||||
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.IntegrationEvents.EventHandling;
|
||||||
|
|
||||||
|
public class OrderCouponConfirmedIntegrationEventHandler : IIntegrationEventHandler<OrderCouponConfirmedIntegrationEvent>
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public OrderCouponConfirmedIntegrationEventHandler(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(OrderCouponConfirmedIntegrationEvent @event)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||||
|
{
|
||||||
|
Log.Information("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||||
|
|
||||||
|
var command = new CouponConfirmedCommand(@event.OrderId, @event.Discount);
|
||||||
|
|
||||||
|
Log.Information("----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
|
||||||
|
command.GetGenericTypeName(),
|
||||||
|
nameof(command.OrderNumber),
|
||||||
|
command.OrderNumber,
|
||||||
|
command);
|
||||||
|
|
||||||
|
await _mediator.Send(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.IntegrationEvents.EventHandling;
|
||||||
|
|
||||||
|
public class OrderCouponRejectedIntegrationEventHandler : IIntegrationEventHandler<OrderCouponRejectedIntegrationEvent>
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public OrderCouponRejectedIntegrationEventHandler(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(OrderCouponRejectedIntegrationEvent @event)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||||
|
{
|
||||||
|
Log.Information("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||||
|
|
||||||
|
Log.Warning("Discount failed, cancelling order {OrderId}", @event.OrderId);
|
||||||
|
|
||||||
|
var command = new CancelOrderCommand(@event.OrderId);
|
||||||
|
|
||||||
|
Log.Information("----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
|
||||||
|
command.GetGenericTypeName(),
|
||||||
|
nameof(command.OrderNumber),
|
||||||
|
command.OrderNumber,
|
||||||
|
command);
|
||||||
|
|
||||||
|
await _mediator.Send(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
|
||||||
|
|
||||||
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
||||||
{
|
{
|
||||||
@ -34,10 +36,11 @@ public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHand
|
|||||||
{
|
{
|
||||||
using (LogContext.PushProperty("IdentifiedCommandId", @event.RequestId))
|
using (LogContext.PushProperty("IdentifiedCommandId", @event.RequestId))
|
||||||
{
|
{
|
||||||
var createOrderCommand = new CreateOrderCommand(@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street,
|
var createOrderCommand = new CreateOrderCommand(
|
||||||
|
@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street,
|
||||||
@event.State, @event.Country, @event.ZipCode,
|
@event.State, @event.Country, @event.ZipCode,
|
||||||
@event.CardNumber, @event.CardHolderName, @event.CardExpiration,
|
@event.CardNumber, @event.CardHolderName, @event.CardExpiration,
|
||||||
@event.CardSecurityNumber, @event.CardTypeId);
|
@event.CardSecurityNumber, @event.CardTypeId, @event.CodeDiscount, @event.Discount);
|
||||||
|
|
||||||
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, @event.RequestId);
|
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, @event.RequestId);
|
||||||
|
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
public record OrderCouponConfirmedIntegrationEvent : IntegrationEvent
|
||||||
|
{
|
||||||
|
[JsonProperty]
|
||||||
|
public int OrderId { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public int Discount { get; private set; }
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
public record OrderCouponRejectedIntegrationEvent : IntegrationEvent
|
||||||
|
{
|
||||||
|
[JsonProperty]
|
||||||
|
public int OrderId { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public string Code { get; private set; }
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
namespace Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent
|
||||||
|
{
|
||||||
|
public int OrderId { get; }
|
||||||
|
|
||||||
|
public string OrderStatus { get; }
|
||||||
|
|
||||||
|
public string BuyerName { get; }
|
||||||
|
|
||||||
|
public string Code { get; set; }
|
||||||
|
|
||||||
|
public OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(int orderId, string orderStatus, string buyerName, string code)
|
||||||
|
{
|
||||||
|
OrderId = orderId;
|
||||||
|
OrderStatus = orderStatus;
|
||||||
|
BuyerName = buyerName;
|
||||||
|
Code = code;
|
||||||
|
}
|
||||||
|
}
|
@ -5,11 +5,13 @@ public record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent
|
|||||||
public int OrderId { get; }
|
public int OrderId { get; }
|
||||||
public string OrderStatus { get; }
|
public string OrderStatus { get; }
|
||||||
public string BuyerName { get; }
|
public string BuyerName { get; }
|
||||||
|
public string DiscountCode { get; }
|
||||||
|
|
||||||
public OrderStatusChangedToCancelledIntegrationEvent(int orderId, string orderStatus, string buyerName)
|
public OrderStatusChangedToCancelledIntegrationEvent(int orderId, string orderStatus, string buyerName, string discountCode)
|
||||||
{
|
{
|
||||||
OrderId = orderId;
|
OrderId = orderId;
|
||||||
OrderStatus = orderStatus;
|
OrderStatus = orderStatus;
|
||||||
BuyerName = buyerName;
|
BuyerName = buyerName;
|
||||||
|
DiscountCode = discountCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,14 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
|||||||
|
|
||||||
public CustomerBasket Basket { get; }
|
public CustomerBasket Basket { get; }
|
||||||
|
|
||||||
|
public string CodeDiscount { get; set; }
|
||||||
|
|
||||||
|
public int Discount { get; set; }
|
||||||
|
|
||||||
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
||||||
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
||||||
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
||||||
CustomerBasket basket)
|
CustomerBasket basket, string codeDiscount, int discount)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
City = city;
|
City = city;
|
||||||
@ -52,6 +56,8 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
|||||||
Basket = basket;
|
Basket = basket;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
UserName = userName;
|
UserName = userName;
|
||||||
|
CodeDiscount = codeDiscount;
|
||||||
|
Discount = discount;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ public class OrderQueries
|
|||||||
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
|
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
|
||||||
os.Name as status,
|
os.Name as status,
|
||||||
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
|
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
|
||||||
|
o.DiscountCode as coupon
|
||||||
|
o.Discount as discount,
|
||||||
FROM ordering.Orders o
|
FROM ordering.Orders o
|
||||||
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
|
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
|
||||||
LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id
|
LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id
|
||||||
@ -39,14 +41,27 @@ public class OrderQueries
|
|||||||
using var connection = new SqlConnection(_connectionString);
|
using var connection = new SqlConnection(_connectionString);
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
return await connection.QueryAsync<OrderSummary>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total
|
return await connection.QueryAsync<OrderSummary>(@"
|
||||||
FROM [ordering].[Orders] o
|
with OrderTotal as (
|
||||||
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
|
select
|
||||||
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
|
OrderId,
|
||||||
LEFT JOIN[ordering].[buyers] ob on o.BuyerId = ob.Id
|
sum(Units * UnitPrice) subtotal
|
||||||
WHERE ob.IdentityGuid = @userId
|
from ordering.orderitems
|
||||||
GROUP BY o.[Id], o.[OrderDate], os.[Name]
|
group by OrderId)
|
||||||
ORDER BY o.[Id]", new { userId });
|
select
|
||||||
|
o.Id ordernumber,
|
||||||
|
o.OrderDate date,
|
||||||
|
os.Name status,
|
||||||
|
case
|
||||||
|
when ot.subtotal > isnull(o.Discount, 0)
|
||||||
|
then ot.subtotal - isnull(o.Discount, 0)
|
||||||
|
else 0
|
||||||
|
end total
|
||||||
|
from ordering.orders o
|
||||||
|
join OrderTotal ot on ot.OrderId = o.Id
|
||||||
|
join ordering.orderstatus os on os.Id = o.OrderStatusId
|
||||||
|
join ordering.buyers b on b.Id = o.BuyerId",
|
||||||
|
new { userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CardType>> GetCardTypesAsync()
|
public async Task<IEnumerable<CardType>> GetCardTypesAsync()
|
||||||
@ -70,6 +85,9 @@ public class OrderQueries
|
|||||||
zipcode = result[0].zipcode,
|
zipcode = result[0].zipcode,
|
||||||
country = result[0].country,
|
country = result[0].country,
|
||||||
orderitems = new List<Orderitem>(),
|
orderitems = new List<Orderitem>(),
|
||||||
|
subtotal = 0,
|
||||||
|
coupon = result[0].coupon,
|
||||||
|
discount = result[0].discount ?? 0m,
|
||||||
total = 0
|
total = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,10 +101,14 @@ public class OrderQueries
|
|||||||
pictureurl = item.pictureurl
|
pictureurl = item.pictureurl
|
||||||
};
|
};
|
||||||
|
|
||||||
order.total += item.units * item.unitprice;
|
order.subtotal += item.units * item.unitprice;
|
||||||
order.orderitems.Add(orderitem);
|
order.orderitems.Add(orderitem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order.total = order.discount < order.subtotal
|
||||||
|
? order.subtotal - order.discount
|
||||||
|
: 0;
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,10 @@ public record Order
|
|||||||
public string zipcode { get; init; }
|
public string zipcode { get; init; }
|
||||||
public string country { get; init; }
|
public string country { get; init; }
|
||||||
public List<Orderitem> orderitems { get; set; }
|
public List<Orderitem> orderitems { get; set; }
|
||||||
|
public decimal subtotal { get; set; }
|
||||||
public decimal total { get; set; }
|
public decimal total { get; set; }
|
||||||
|
public string coupon { get; set; }
|
||||||
|
public decimal discount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record OrderSummary
|
public record OrderSummary
|
||||||
|
@ -132,6 +132,7 @@ public class OrderingContextSeed
|
|||||||
OrderStatus.Submitted,
|
OrderStatus.Submitted,
|
||||||
OrderStatus.AwaitingValidation,
|
OrderStatus.AwaitingValidation,
|
||||||
OrderStatus.StockConfirmed,
|
OrderStatus.StockConfirmed,
|
||||||
|
OrderStatus.AwaitingCouponValidation,
|
||||||
OrderStatus.Paid,
|
OrderStatus.Paid,
|
||||||
OrderStatus.Shipped,
|
OrderStatus.Shipped,
|
||||||
OrderStatus.Cancelled
|
OrderStatus.Cancelled
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API;
|
namespace Microsoft.eShopOnContainers.Services.Ordering.API;
|
||||||
|
|
||||||
public class Startup
|
public class Startup
|
||||||
@ -106,6 +108,9 @@ public class Startup
|
|||||||
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
||||||
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
|
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
|
||||||
eventBus.Subscribe<OrderPaymentSucceededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>>();
|
eventBus.Subscribe<OrderPaymentSucceededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>>();
|
||||||
|
|
||||||
|
eventBus.Subscribe<OrderCouponRejectedIntegrationEvent, IIntegrationEventHandler<OrderCouponRejectedIntegrationEvent>>();
|
||||||
|
eventBus.Subscribe<OrderCouponConfirmedIntegrationEvent, IIntegrationEventHandler<OrderCouponConfirmedIntegrationEvent>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
|
||||||
|
using Ordering.Domain.Events;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
|
||||||
@ -17,10 +18,16 @@ public class Order
|
|||||||
private int? _buyerId;
|
private int? _buyerId;
|
||||||
|
|
||||||
public OrderStatus OrderStatus { get; private set; }
|
public OrderStatus OrderStatus { get; private set; }
|
||||||
|
|
||||||
|
public bool? DiscountConfirmed { get; private set; }
|
||||||
|
|
||||||
private int _orderStatusId;
|
private int _orderStatusId;
|
||||||
|
|
||||||
private string _description;
|
private string _description;
|
||||||
|
|
||||||
|
public string DiscountCode { get; private set; }
|
||||||
|
|
||||||
|
public decimal? Discount { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
// Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed
|
// Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed
|
||||||
@ -49,13 +56,15 @@ public class Order
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
|
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
|
||||||
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
|
string cardHolderName, DateTime cardExpiration, string discountCode, decimal? discount, int? buyerId = null, int? paymentMethodId = null) : this()
|
||||||
{
|
{
|
||||||
_buyerId = buyerId;
|
_buyerId = buyerId;
|
||||||
_paymentMethodId = paymentMethodId;
|
_paymentMethodId = paymentMethodId;
|
||||||
_orderStatusId = OrderStatus.Submitted.Id;
|
_orderStatusId = OrderStatus.Submitted.Id;
|
||||||
_orderDate = DateTime.UtcNow;
|
_orderDate = DateTime.UtcNow;
|
||||||
Address = address;
|
Address = address;
|
||||||
|
DiscountCode = discountCode;
|
||||||
|
Discount = discountCode == null ? null : discount;
|
||||||
|
|
||||||
// Add the OrderStarterDomainEvent to the domain events collection
|
// Add the OrderStarterDomainEvent to the domain events collection
|
||||||
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
|
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
|
||||||
@ -113,13 +122,44 @@ public class Order
|
|||||||
|
|
||||||
public void SetStockConfirmedStatus()
|
public void SetStockConfirmedStatus()
|
||||||
{
|
{
|
||||||
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
|
// If there's no Couponm, then it's validated
|
||||||
|
if (DiscountCode == null)
|
||||||
{
|
{
|
||||||
AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));
|
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
|
||||||
|
{
|
||||||
|
AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));
|
||||||
|
|
||||||
_orderStatusId = OrderStatus.StockConfirmed.Id;
|
_orderStatusId = OrderStatus.StockConfirmed.Id;
|
||||||
_description = "All the items were confirmed with available stock.";
|
_description = "All the items were confirmed with available stock.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_orderStatusId != OrderStatus.AwaitingValidation.Id)
|
||||||
|
{
|
||||||
|
StatusChangeException(OrderStatus.AwaitingCouponValidation);
|
||||||
|
}
|
||||||
|
|
||||||
|
_orderStatusId = OrderStatus.AwaitingCouponValidation.Id;
|
||||||
|
_description = "Validate discount code";
|
||||||
|
|
||||||
|
AddDomainEvent(new OrderStatusChangedToAwaitingCouponValidationDomainEvent(Id, DiscountCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessCouponConfirmed()
|
||||||
|
{
|
||||||
|
if (_orderStatusId != OrderStatus.AwaitingCouponValidation.Id)
|
||||||
|
{
|
||||||
|
StatusChangeException(OrderStatus.StockConfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscountConfirmed = true;
|
||||||
|
|
||||||
|
_orderStatusId = OrderStatus.StockConfirmed.Id;
|
||||||
|
_description = "Discount coupon validated.";
|
||||||
|
|
||||||
|
AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPaidStatus()
|
public void SetPaidStatus()
|
||||||
@ -190,6 +230,8 @@ public class Order
|
|||||||
|
|
||||||
public decimal GetTotal()
|
public decimal GetTotal()
|
||||||
{
|
{
|
||||||
return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice());
|
var result = _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice()) - (Discount ?? 0);
|
||||||
|
|
||||||
|
return result < 0 ? 0 : result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,11 @@ public class OrderStatus
|
|||||||
{
|
{
|
||||||
public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
|
public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
|
||||||
public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
|
public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
|
||||||
public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
|
public static OrderStatus AwaitingCouponValidation = new OrderStatus(3, nameof(AwaitingCouponValidation).ToLowerInvariant());
|
||||||
public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
|
public static OrderStatus StockConfirmed = new OrderStatus(4, nameof(StockConfirmed).ToLowerInvariant());
|
||||||
public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
|
public static OrderStatus Paid = new OrderStatus(5, nameof(Paid).ToLowerInvariant());
|
||||||
public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());
|
public static OrderStatus Shipped = new OrderStatus(6, nameof(Shipped).ToLowerInvariant());
|
||||||
|
public static OrderStatus Cancelled = new OrderStatus(7, nameof(Cancelled).ToLowerInvariant());
|
||||||
|
|
||||||
public OrderStatus(int id, string name)
|
public OrderStatus(int id, string name)
|
||||||
: base(id, name)
|
: base(id, name)
|
||||||
@ -18,7 +19,7 @@ public class OrderStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<OrderStatus> List() =>
|
public static IEnumerable<OrderStatus> List() =>
|
||||||
new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled };
|
new[] { Submitted, AwaitingValidation, StockConfirmed, AwaitingCouponValidation, Paid, Shipped, Cancelled };
|
||||||
|
|
||||||
public static OrderStatus FromName(string name)
|
public static OrderStatus FromName(string name)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.Domain.Events;
|
||||||
|
public class OrderStatusChangedToAwaitingCouponValidationDomainEvent : INotification
|
||||||
|
{
|
||||||
|
public int OrderId { get; }
|
||||||
|
|
||||||
|
public string Code { get; set; }
|
||||||
|
|
||||||
|
public OrderStatusChangedToAwaitingCouponValidationDomainEvent(int orderId, string code)
|
||||||
|
{
|
||||||
|
OrderId = orderId;
|
||||||
|
Code = code;
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,10 @@ class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
|
|||||||
|
|
||||||
orderConfiguration.Property<string>("Description").IsRequired(false);
|
orderConfiguration.Property<string>("Description").IsRequired(false);
|
||||||
|
|
||||||
|
orderConfiguration
|
||||||
|
.Property(e => e.DiscountCode)
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
|
var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
|
||||||
|
|
||||||
// DDD Patterns comment:
|
// DDD Patterns comment:
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
using Ordering.SignalrHub.IntegrationEvents.Events;
|
||||||
|
|
||||||
|
namespace Ordering.SignalrHub.IntegrationEvents.EventHandling;
|
||||||
|
|
||||||
|
public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>
|
||||||
|
{
|
||||||
|
private readonly IHubContext<NotificationsHub> _hubContext;
|
||||||
|
private readonly ILogger<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler> _logger;
|
||||||
|
|
||||||
|
public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler(
|
||||||
|
IHubContext<NotificationsHub> hubContext,
|
||||||
|
ILogger<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler> logger)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Handle(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent @event)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||||
|
|
||||||
|
await _hubContext.Clients
|
||||||
|
.Group(@event.BuyerName)
|
||||||
|
.SendAsync("UpdatedOrderState", new { OrderId = @event.OrderId, Status = @event.OrderStatus });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Ordering.SignalrHub.IntegrationEvents.Events;
|
||||||
|
public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent
|
||||||
|
{
|
||||||
|
[JsonProperty]
|
||||||
|
public int OrderId { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public string OrderStatus { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public string BuyerName { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public string Code { get; private set; }
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
using Ordering.SignalrHub.IntegrationEvents.EventHandling;
|
||||||
|
using Ordering.SignalrHub.IntegrationEvents.Events;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub;
|
namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub;
|
||||||
|
|
||||||
public class Startup
|
public class Startup
|
||||||
@ -142,6 +145,7 @@ public class Startup
|
|||||||
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
|
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
|
||||||
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();
|
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();
|
||||||
eventBus.Subscribe<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();
|
eventBus.Subscribe<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();
|
||||||
|
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureAuthService(IServiceCollection services)
|
private void ConfigureAuthService(IServiceCollection services)
|
||||||
|
@ -74,6 +74,8 @@ public class IdentifiedCommandHandlerTest
|
|||||||
cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue,
|
cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue,
|
||||||
cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123",
|
cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123",
|
||||||
cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX",
|
cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX",
|
||||||
cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0);
|
cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0,
|
||||||
|
codeDiscount: args != null && args.ContainsKey("codeDiscount") ? (string)args["codeDiscount"] : "",
|
||||||
|
discount: args != null && args.ContainsKey("discount") ? (int)args["discount"] : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ public class NewOrderRequestHandlerTest
|
|||||||
|
|
||||||
private Order FakeOrder()
|
private Order FakeOrder()
|
||||||
{
|
{
|
||||||
return new Order("1", "fakeName", new Address("street", "city", "state", "country", "zipcode"), 1, "12", "111", "fakeName", DateTime.Now.AddYears(1));
|
return new Order("1", "fakeName", new Address("street", "city", "state", "country", "zipcode"), 1, "12", "111", "fakeName", DateTime.Now.AddYears(1), "", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary<string, object> args = null)
|
private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary<string, object> args = null)
|
||||||
@ -78,6 +78,8 @@ public class NewOrderRequestHandlerTest
|
|||||||
cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue,
|
cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue,
|
||||||
cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123",
|
cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123",
|
||||||
cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX",
|
cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX",
|
||||||
cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0);
|
cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0,
|
||||||
|
codeDiscount: args != null && args.ContainsKey("codeDiscount") ? (string)args["codeDiscount"] : "",
|
||||||
|
discount: args != null && args.ContainsKey("discount") ? (int)args["discount"] : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,9 @@ public class OrderBuilder
|
|||||||
cardNumber: "12",
|
cardNumber: "12",
|
||||||
cardSecurityNumber: "123",
|
cardSecurityNumber: "123",
|
||||||
cardHolderName: "name",
|
cardHolderName: "name",
|
||||||
cardExpiration: DateTime.UtcNow);
|
cardExpiration: DateTime.UtcNow,
|
||||||
|
"",
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OrderBuilder AddOne(
|
public OrderBuilder AddOne(
|
||||||
|
@ -119,7 +119,7 @@ public class OrderAggregateTest
|
|||||||
var expectedResult = 1;
|
var expectedResult = 1;
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);
|
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration, "", 0);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult);
|
Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult);
|
||||||
@ -142,7 +142,7 @@ public class OrderAggregateTest
|
|||||||
var expectedResult = 2;
|
var expectedResult = 2;
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);
|
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration, "", 0);
|
||||||
fakeOrder.AddDomainEvent(new OrderStartedDomainEvent(fakeOrder, "fakeName", "1", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration));
|
fakeOrder.AddDomainEvent(new OrderStartedDomainEvent(fakeOrder, "fakeName", "1", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration));
|
||||||
//Assert
|
//Assert
|
||||||
Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult);
|
Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult);
|
||||||
@ -162,7 +162,7 @@ public class OrderAggregateTest
|
|||||||
var cardSecurityNumber = "123";
|
var cardSecurityNumber = "123";
|
||||||
var cardHolderName = "FakeName";
|
var cardHolderName = "FakeName";
|
||||||
var cardExpiration = DateTime.Now.AddYears(1);
|
var cardExpiration = DateTime.Now.AddYears(1);
|
||||||
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);
|
var fakeOrder = new Order("1", "fakeName", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration, "", 0);
|
||||||
var @fakeEvent = new OrderStartedDomainEvent(fakeOrder, "1", "fakeName", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);
|
var @fakeEvent = new OrderStartedDomainEvent(fakeOrder, "1", "fakeName", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);
|
||||||
var expectedResult = 1;
|
var expectedResult = 1;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user