diff --git a/src/ApiGateways/Envoy/config/webshopping/envoy.yaml b/src/ApiGateways/Envoy/config/webshopping/envoy.yaml index 688fb740c..6c82e2e20 100644 --- a/src/ApiGateways/Envoy/config/webshopping/envoy.yaml +++ b/src/ApiGateways/Envoy/config/webshopping/envoy.yaml @@ -72,6 +72,19 @@ static_resources: route: auto_host_rewrite: true 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" match: prefix: "/" @@ -140,3 +153,11 @@ static_resources: - socket_address: address: ordering-signalrhub 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 \ No newline at end of file diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Config/UrlsConfig.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Config/UrlsConfig.cs index 9b3391692..c5e821e4c 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Config/UrlsConfig.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Config/UrlsConfig.cs @@ -37,5 +37,7 @@ public class UrlsConfig public string GrpcCatalog { get; set; } public string GrpcOrdering { get; set; } + + public string Coupon { get; set; } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs index 143ff9a2b..3f9200142 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs @@ -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]")] [Authorize] @@ -7,11 +9,13 @@ public class BasketController : ControllerBase { private readonly ICatalogService _catalog; private readonly IBasketService _basket; + private readonly ICouponService _coupon; - public BasketController(ICatalogService catalogService, IBasketService basketService) + public BasketController(ICatalogService catalogService, IBasketService basketService, ICouponService couponService) { _catalog = catalogService; _basket = basketService; + _coupon = couponService; } [HttpPost] @@ -151,4 +155,24 @@ public class BasketController : ControllerBase return Ok(); } + + [HttpGet] + [Route("coupon/{code}")] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType(typeof(CouponData), (int)HttpStatusCode.OK)] + public async Task> CheckCouponAsync(string code) + { + var response = await _coupon.CheckCouponByCodeNumberAsync(code); + + if (!response.IsSuccessStatusCode) + { + return NotFound(); + } + + var couponResponse = await response.Content.ReadAsStringAsync(); + + var data = JsonConvert.DeserializeObject(couponResponse); + + return Ok(data); + } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Models/CouponData.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Models/CouponData.cs new file mode 100644 index 000000000..271151abf --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Models/CouponData.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; +public class CouponData +{ + public int Discount { get; set; } + + public string Code { get; set; } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/CouponService.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/CouponService.cs new file mode 100644 index 000000000..3d5a75e29 --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/CouponService.cs @@ -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 _logger; + + public CouponService(HttpClient httpClient, IOptions config, ILogger logger) + { + _urls = config.Value; + _httpClient = httpClient; + _logger = logger; + } + + public async Task 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; + } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/ICouponService.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/ICouponService.cs new file mode 100644 index 000000000..60904f149 --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/ICouponService.cs @@ -0,0 +1,6 @@ +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; + +public interface ICouponService +{ + Task CheckCouponByCodeNumberAsync(string codeNumber); +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs index 6e8e66931..edbb232f4 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; 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. public void ConfigureServices(IServiceCollection services) { - services.AddHealthChecks() + var healthCheckBuilder = services.AddHealthChecks() .AddCheck("self", () => HealthCheckResult.Healthy()) .AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) .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["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) .AddCustomAuthentication(Configuration) .AddDevspaces() @@ -163,6 +172,9 @@ public static class ServiceCollectionExtensions .AddHttpMessageHandler() .AddDevspacesSupport(); + services.AddHttpClient() + . AddHttpMessageHandler(); + return services; } diff --git a/src/Services/Basket/Basket.API/Controllers/BasketController.cs b/src/Services/Basket/Basket.API/Controllers/BasketController.cs index 5468bbc15..77607555d 100644 --- a/src/Services/Basket/Basket.API/Controllers/BasketController.cs +++ b/src/Services/Basket/Basket.API/Controllers/BasketController.cs @@ -58,9 +58,24 @@ public class BasketController : ControllerBase var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value; - var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, 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); + var eventMessage = new UserCheckoutAcceptedIntegrationEvent( + userId, + 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 // ordering.api to convert basket to order and proceeds with diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs index d57a32c25..33ce87b0e 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs @@ -34,10 +34,16 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent public CustomerBasket Basket { get; } + public string CodeDiscount { get; set; } + + public int Discount { get; set; } + public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street, string state, string country, string zipCode, string cardNumber, string cardHolderName, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, - CustomerBasket basket) + CustomerBasket basket, + string codeDiscount, + int discount) { UserId = userId; UserName = userName; @@ -54,6 +60,8 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent Buyer = buyer; Basket = basket; RequestId = requestId; + CodeDiscount = codeDiscount; + Discount = discount; } } diff --git a/src/Services/Basket/Basket.API/Model/BasketCheckout.cs b/src/Services/Basket/Basket.API/Model/BasketCheckout.cs index 5340444d7..5a7ddbc4c 100644 --- a/src/Services/Basket/Basket.API/Model/BasketCheckout.cs +++ b/src/Services/Basket/Basket.API/Model/BasketCheckout.cs @@ -25,4 +25,8 @@ public class BasketCheckout public string Buyer { get; set; } public Guid RequestId { get; set; } + + public string Coupon { get; set; } + + public int Discount { get; set; } } diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 6d5393bde..b3f758eaa 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -15,6 +15,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration new ApiResource("webshoppingagg", "Web Shopping Aggregator"), new ApiResource("orders.signalrhub", "Ordering Signalr Hub"), new ApiResource("webhooks", "Webhooks registration Service"), + new ApiResource("coupon", "Coupon Service"), }; } @@ -53,7 +54,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration "basket", "webshoppingagg", "orders.signalrhub", - "webhooks" + "webhooks", + "coupon" }, }, new Client @@ -79,7 +81,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration "orders", "basket", "mobileshoppingagg", - "webhooks" + "webhooks", + "coupon" }, //Allow requesting refresh tokens for long lived API access AllowOfflineAccess = true, @@ -117,7 +120,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration "basket", "webshoppingagg", "orders.signalrhub", - "webhooks" + "webhooks", + "coupon" }, AccessTokenLifetime = 60*60*2, // 2 hours IdentityTokenLifetime= 60*60*2 // 2 hours @@ -183,7 +187,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration "orders", "basket", "webshoppingagg", - "webhooks" + "webhooks", + "coupon" }, }, new Client @@ -261,6 +266,21 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration { "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" + } } }; } diff --git a/src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs b/src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs index e3df90fdd..b0199d8ce 100644 --- a/src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs +++ b/src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs @@ -19,6 +19,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Data clientUrls.Add("WebShoppingAgg", configuration.GetValue("WebShoppingAggClient")); clientUrls.Add("WebhooksApi", configuration.GetValue("WebhooksApiClient")); clientUrls.Add("WebhooksWeb", configuration.GetValue("WebhooksWebClient")); + clientUrls.Add("CouponApi", configuration.GetValue("CouponApiClient")); if (!context.Clients.Any()) { diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommand.cs new file mode 100644 index 000000000..99feb9afa --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommand.cs @@ -0,0 +1,17 @@ +namespace Ordering.API.Application.Commands; + +public class CouponConfirmedCommand : IRequest +{ + + [DataMember] + public int OrderNumber { get; private set; } + + [DataMember] + public int Discount { get; private set; } + + public CouponConfirmedCommand(int orderNumber, int discount) + { + OrderNumber = orderNumber; + Discount = discount; + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommandHandler.cs new file mode 100644 index 000000000..5b362c515 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CouponConfirmedCommandHandler.cs @@ -0,0 +1,41 @@ +namespace Ordering.API.Application.Commands; + +public class CouponConfirmedCommandHandler : IRequestHandler +{ + private readonly IOrderRepository _orderRepository; + + public CouponConfirmedCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task 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 +{ + public CouponConfirmIdenfifiedCommandHandler( + IMediator mediator, + IRequestManager requestManager, + ILogger> logger) + : base(mediator, requestManager, logger) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs index fb16306ee..4091e5d96 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs @@ -54,6 +54,12 @@ public class CreateOrderCommand [DataMember] public int CardTypeId { get; private set; } + [DataMember] + public string CodeDiscount { get; private set; } + + [DataMember] + public decimal Discount { get; private set; } + [DataMember] public IEnumerable OrderItems => _orderItems; @@ -64,7 +70,7 @@ public class CreateOrderCommand public CreateOrderCommand(List basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode, string cardNumber, string cardHolderName, DateTime cardExpiration, - string cardSecurityNumber, int cardTypeId) : this() + string cardSecurityNumber, int cardTypeId, string codeDiscount, decimal discount) : this() { _orderItems = basketItems.ToOrderItemsDTO().ToList(); UserId = userId; @@ -79,6 +85,8 @@ public class CreateOrderCommand CardExpiration = cardExpiration; CardSecurityNumber = cardSecurityNumber; CardTypeId = cardTypeId; + CodeDiscount = codeDiscount; + Discount = discount; } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs index e445e5de1..3b145dd99 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs @@ -37,7 +37,7 @@ public class CreateOrderCommandHandler // methods and constructor so validations, invariants and business logic // 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 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) { diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs index dbd8abceb..d63d2e127 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs @@ -31,7 +31,7 @@ public class OrderCancelledDomainEventHandler var order = await _orderRepository.GetAsync(orderCancelledDomainEvent.Order.Id); 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); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCoupon/OrderStatusChangedToAwaitingCouponValidationDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCoupon/OrderStatusChangedToAwaitingCouponValidationDomainEventHandler.cs new file mode 100644 index 000000000..f1d82ffb4 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCoupon/OrderStatusChangedToAwaitingCouponValidationDomainEventHandler.cs @@ -0,0 +1,37 @@ +using Ordering.API.Application.IntegrationEvents.Events; +using Ordering.Domain.Events; + +namespace Ordering.API.Application.DomainEventHandlers.OrderCoupon; + +public class OrderStatusChangedToAwaitingCouponValidationDomainEventHandler : INotificationHandler +{ + 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() + .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); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponConfirmedIntegrationEventHandler.cs new file mode 100644 index 000000000..7ef9a6aa7 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponConfirmedIntegrationEventHandler.cs @@ -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 +{ + 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); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponRejectedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponRejectedIntegrationEventHandler.cs new file mode 100644 index 000000000..ced1f5a21 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponRejectedIntegrationEventHandler.cs @@ -0,0 +1,33 @@ +using Ordering.API.Application.IntegrationEvents.Events; + +namespace Ordering.API.Application.IntegrationEvents.EventHandling; + +public class OrderCouponRejectedIntegrationEventHandler : IIntegrationEventHandler +{ + 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); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs index a5a15c06c..1be0303be 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs @@ -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 { @@ -34,10 +36,11 @@ public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHand { 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.CardNumber, @event.CardHolderName, @event.CardExpiration, - @event.CardSecurityNumber, @event.CardTypeId); + @event.CardNumber, @event.CardHolderName, @event.CardExpiration, + @event.CardSecurityNumber, @event.CardTypeId, @event.CodeDiscount, @event.Discount); var requestCreateOrder = new IdentifiedCommand(createOrderCommand, @event.RequestId); diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..653f69d9f --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs new file mode 100644 index 000000000..616b6a028 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs new file mode 100644 index 000000000..21eb317c5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs index 170bc2d06..67849c3b9 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs @@ -5,11 +5,13 @@ public record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent public int OrderId { get; } public string OrderStatus { 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; OrderStatus = orderStatus; BuyerName = buyerName; + DiscountCode = discountCode; } } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs index 811be0ec4..d63213c82 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs @@ -32,10 +32,14 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent public CustomerBasket Basket { get; } + public string CodeDiscount { get; set; } + + public int Discount { get; set; } + public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street, string state, string country, string zipCode, string cardNumber, string cardHolderName, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, - CustomerBasket basket) + CustomerBasket basket, string codeDiscount, int discount) { UserId = userId; City = city; @@ -51,7 +55,9 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent Buyer = buyer; Basket = basket; RequestId = requestId; - UserName = userName; + UserName = userName; + CodeDiscount = codeDiscount; + Discount = discount; } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index 860c587c4..8871a441f 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -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, os.Name as status, 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 LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id @@ -39,14 +41,27 @@ public class OrderQueries using var connection = new SqlConnection(_connectionString); connection.Open(); - return await connection.QueryAsync(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total - FROM [ordering].[Orders] o - LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid - LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id - LEFT JOIN[ordering].[buyers] ob on o.BuyerId = ob.Id - WHERE ob.IdentityGuid = @userId - GROUP BY o.[Id], o.[OrderDate], os.[Name] - ORDER BY o.[Id]", new { userId }); + return await connection.QueryAsync(@" + with OrderTotal as ( + select + OrderId, + sum(Units * UnitPrice) subtotal + from ordering.orderitems + group by OrderId) + 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> GetCardTypesAsync() @@ -70,6 +85,9 @@ public class OrderQueries zipcode = result[0].zipcode, country = result[0].country, orderitems = new List(), + subtotal = 0, + coupon = result[0].coupon, + discount = result[0].discount ?? 0m, total = 0 }; @@ -83,10 +101,14 @@ public class OrderQueries pictureurl = item.pictureurl }; - order.total += item.units * item.unitprice; + order.subtotal += item.units * item.unitprice; order.orderitems.Add(orderitem); } + order.total = order.discount < order.subtotal + ? order.subtotal - order.discount + : 0; + return order; } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs index d5ce86005..208546a92 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs @@ -19,7 +19,10 @@ public record Order public string zipcode { get; init; } public string country { get; init; } public List orderitems { get; set; } + public decimal subtotal { get; set; } public decimal total { get; set; } + public string coupon { get; set; } + public decimal discount { get; set; } } public record OrderSummary diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs index 3c0aca009..439ee2c50 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs @@ -132,6 +132,7 @@ public class OrderingContextSeed OrderStatus.Submitted, OrderStatus.AwaitingValidation, OrderStatus.StockConfirmed, + OrderStatus.AwaitingCouponValidation, OrderStatus.Paid, OrderStatus.Shipped, OrderStatus.Cancelled diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index a4bfacca1..48807ed24 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -1,3 +1,5 @@ +using Ordering.API.Application.IntegrationEvents.Events; + namespace Microsoft.eShopOnContainers.Services.Ordering.API; public class Startup @@ -106,6 +108,9 @@ public class Startup eventBus.Subscribe>(); eventBus.Subscribe>(); eventBus.Subscribe>(); + + eventBus.Subscribe>(); + eventBus.Subscribe>(); } protected virtual void ConfigureAuth(IApplicationBuilder app) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 81a67324b..edd464ed9 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -1,4 +1,5 @@ using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; +using Ordering.Domain.Events; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; @@ -17,10 +18,16 @@ public class Order private int? _buyerId; public OrderStatus OrderStatus { get; private set; } + + public bool? DiscountConfirmed { get; private set; } + private int _orderStatusId; 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 @@ -49,13 +56,15 @@ public class Order } 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; _paymentMethodId = paymentMethodId; _orderStatusId = OrderStatus.Submitted.Id; _orderDate = DateTime.UtcNow; Address = address; + DiscountCode = discountCode; + Discount = discountCode == null ? null : discount; // Add the OrderStarterDomainEvent to the domain events collection // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ] @@ -113,15 +122,46 @@ public class Order 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; + _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"; - _orderStatusId = OrderStatus.StockConfirmed.Id; - _description = "All the items were confirmed with available stock."; + 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() { if (_orderStatusId == OrderStatus.StockConfirmed.Id) @@ -190,6 +230,8 @@ public class Order 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; } } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs index aae09bc6a..06d1347f2 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -7,10 +7,11 @@ public class OrderStatus { public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); - public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant()); - public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant()); - public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant()); - public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant()); + public static OrderStatus AwaitingCouponValidation = new OrderStatus(3, nameof(AwaitingCouponValidation).ToLowerInvariant()); + public static OrderStatus StockConfirmed = new OrderStatus(4, nameof(StockConfirmed).ToLowerInvariant()); + public static OrderStatus Paid = new OrderStatus(5, nameof(Paid).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) : base(id, name) @@ -18,7 +19,7 @@ public class OrderStatus } public static IEnumerable List() => - new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled }; + new[] { Submitted, AwaitingValidation, StockConfirmed, AwaitingCouponValidation, Paid, Shipped, Cancelled }; public static OrderStatus FromName(string name) { diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingCouponValidationDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingCouponValidationDomainEvent.cs new file mode 100644 index 000000000..5ce776bf2 --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingCouponValidationDomainEvent.cs @@ -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; + } +} diff --git a/src/Services/Ordering/Ordering.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs b/src/Services/Ordering/Ordering.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs index e8fe7d97b..332fd5cbb 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs @@ -51,6 +51,10 @@ class OrderEntityTypeConfiguration : IEntityTypeConfiguration orderConfiguration.Property("Description").IsRequired(false); + orderConfiguration + .Property(e => e.DiscountCode) + .HasMaxLength(50); + var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems)); // DDD Patterns comment: diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs new file mode 100644 index 000000000..57ce9c4d5 --- /dev/null +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs @@ -0,0 +1,30 @@ +using Ordering.SignalrHub.IntegrationEvents.Events; + +namespace Ordering.SignalrHub.IntegrationEvents.EventHandling; + +public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler : IIntegrationEventHandler +{ + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler( + IHubContext hubContext, + ILogger 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 }); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs new file mode 100644 index 000000000..eef8a3c48 --- /dev/null +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs @@ -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; } +} diff --git a/src/Services/Ordering/Ordering.SignalrHub/Startup.cs b/src/Services/Ordering/Ordering.SignalrHub/Startup.cs index b57f4477e..ace144cc3 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Startup.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/Startup.cs @@ -1,3 +1,6 @@ +using Ordering.SignalrHub.IntegrationEvents.EventHandling; +using Ordering.SignalrHub.IntegrationEvents.Events; + namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub; public class Startup @@ -142,6 +145,7 @@ public class Startup eventBus.Subscribe(); eventBus.Subscribe(); eventBus.Subscribe(); + eventBus.Subscribe(); } private void ConfigureAuthService(IServiceCollection services) diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs index 1b7d38864..57ddb8f93 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs @@ -74,6 +74,8 @@ public class IdentifiedCommandHandlerTest cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue, cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123", 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); } } diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs index 184a5f5d2..af80cacd7 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs @@ -60,11 +60,11 @@ public class NewOrderRequestHandlerTest 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 args = null) - { + { return new CreateOrderCommand( new List(), userId: args != null && args.ContainsKey("userId") ? (string)args["userId"] : null, @@ -78,6 +78,8 @@ public class NewOrderRequestHandlerTest cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue, cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123", 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); } } diff --git a/src/Services/Ordering/Ordering.UnitTests/Builders.cs b/src/Services/Ordering/Ordering.UnitTests/Builders.cs index 8ecd66cbd..858398a8d 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Builders.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Builders.cs @@ -24,7 +24,9 @@ public class OrderBuilder cardNumber: "12", cardSecurityNumber: "123", cardHolderName: "name", - cardExpiration: DateTime.UtcNow); + cardExpiration: DateTime.UtcNow, + "", + 0); } public OrderBuilder AddOne( diff --git a/src/Services/Ordering/Ordering.UnitTests/Domain/OrderAggregateTest.cs b/src/Services/Ordering/Ordering.UnitTests/Domain/OrderAggregateTest.cs index 676c05fd8..3e870b2fd 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Domain/OrderAggregateTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Domain/OrderAggregateTest.cs @@ -119,7 +119,7 @@ public class OrderAggregateTest var expectedResult = 1; //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.Equal(fakeOrder.DomainEvents.Count, expectedResult); @@ -142,7 +142,7 @@ public class OrderAggregateTest var expectedResult = 2; //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)); //Assert Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult); @@ -162,7 +162,7 @@ public class OrderAggregateTest var cardSecurityNumber = "123"; var cardHolderName = "FakeName"; 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 expectedResult = 1;