@ -0,0 +1,8 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard1.4</TargetFramework> | |||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.CommandBus</RootNamespace> | |||
</PropertyGroup> | |||
</Project> |
@ -0,0 +1,13 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus | |||
{ | |||
public interface ICommandBus | |||
{ | |||
Task SendAsync<T>(T command) where T : IntegrationCommand; | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus | |||
{ | |||
public abstract class IntegrationCommand | |||
{ | |||
public Guid Id { get; private set; } | |||
public DateTime Sent { get; private set; } | |||
protected IntegrationCommand() | |||
{ | |||
Id = Guid.NewGuid(); | |||
Sent = DateTime.UtcNow; | |||
} | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard1.4</TargetFramework> | |||
</PropertyGroup> | |||
</Project> |
@ -0,0 +1,8 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard1.0</TargetFramework> | |||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.CommandBus</RootNamespace> | |||
</PropertyGroup> | |||
</Project> |
@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus | |||
{ | |||
public interface ICommandBus | |||
{ | |||
void Send<T>(string name, T data); | |||
void Handle<TC>(string name, IIntegrationCommandHandler<TC> handler); | |||
void Handle(string name, IIntegrationCommandHandler handler); | |||
void Handle<TI, TC>(TI handler) | |||
where TI : IIntegrationCommandHandler<TC>; | |||
} | |||
} |
@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus | |||
{ | |||
public interface IIntegrationCommandHandler | |||
{ | |||
void Handle(IntegrationCommand command); | |||
} | |||
public interface IIntegrationCommandHandler<T> : IIntegrationCommandHandler | |||
{ | |||
void Handle(IntegrationCommand<T> command); | |||
} | |||
} |
@ -0,0 +1,35 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus | |||
{ | |||
public abstract class IntegrationCommand | |||
{ | |||
public Guid Id { get; } | |||
public DateTime Sent { get; } | |||
public abstract object GetDataAsObject(); | |||
protected IntegrationCommand() | |||
{ | |||
Id = Guid.NewGuid(); | |||
Sent = DateTime.UtcNow; | |||
} | |||
} | |||
public class IntegrationCommand<T> : IntegrationCommand | |||
{ | |||
public T Data { get; } | |||
public string Name { get; } | |||
public override object GetDataAsObject() => Data; | |||
public IntegrationCommand(string name, T data) : base() | |||
{ | |||
Data = data; | |||
Name = name; | |||
} | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||
{ | |||
public interface IDynamicIntegrationEventHandler | |||
{ | |||
Task Handle(dynamic eventData); | |||
} | |||
} |
@ -0,0 +1,28 @@ | |||
using System; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus | |||
{ | |||
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager | |||
{ | |||
public class SubscriptionInfo | |||
{ | |||
public bool IsDynamic { get; } | |||
public Type HandlerType{ get; } | |||
private SubscriptionInfo(bool isDynamic, Type handlerType) | |||
{ | |||
IsDynamic = isDynamic; | |||
HandlerType = handlerType; | |||
} | |||
public static SubscriptionInfo Dynamic(Type handlerType) | |||
{ | |||
return new SubscriptionInfo(true, handlerType); | |||
} | |||
public static SubscriptionInfo Typed(Type handlerType) | |||
{ | |||
return new SubscriptionInfo(false, handlerType); | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,145 @@ | |||
//using Microsoft.eShopOnContainers.BuildingBlocks.CommandBus; | |||
using Microsoft.Extensions.Logging; | |||
using Newtonsoft.Json; | |||
using Polly; | |||
using Polly.Retry; | |||
using RabbitMQ.Client; | |||
using RabbitMQ.Client.Events; | |||
using RabbitMQ.Client.Exceptions; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Net.Sockets; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
/* | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ | |||
{ | |||
public class CommandBusRabbitMQ : ICommandBus, IDisposable | |||
{ | |||
const string BROKER_NAME = "eshop_command_bus"; | |||
private readonly IRabbitMQPersistentConnection _persistentConnection; | |||
private readonly ILogger<CommandBusRabbitMQ> _logger; | |||
private IModel _consumerChannel; | |||
private string _queueName; | |||
private readonly Dictionary<string, IIntegrationCommandHandler> _handlers; | |||
private readonly Dictionary<string, Type> _typeMappings; | |||
public CommandBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, | |||
ILogger<CommandBusRabbitMQ> logger) | |||
{ | |||
_logger = logger; | |||
_persistentConnection = persistentConnection; | |||
_handlers = new Dictionary<string, IIntegrationCommandHandler>(); | |||
_typeMappings = new Dictionary<string, Type>(); | |||
} | |||
public void Send<T>(string name, T data) | |||
{ | |||
Send(new IntegrationCommand<T>(name, data)); | |||
} | |||
public void Handle<TC>(string name, IIntegrationCommandHandler<TC> handler) | |||
{ | |||
_handlers.Add(name, handler); | |||
_typeMappings.Add(name, typeof(TC)); | |||
} | |||
public void Handle(string name, IIntegrationCommandHandler handler) | |||
{ | |||
_handlers.Add(name, handler); | |||
} | |||
public void Handle<TI, TC>(TI handler) where TI : IIntegrationCommandHandler<TC> | |||
{ | |||
var name = typeof(TI).Name; | |||
_handlers.Add(name, handler); | |||
_typeMappings.Add(name, typeof(TC)); | |||
} | |||
private void Send<T>(IntegrationCommand<T> command) | |||
{ | |||
if (!_persistentConnection.IsConnected) | |||
{ | |||
_persistentConnection.TryConnect(); | |||
} | |||
var policy = RetryPolicy.Handle<BrokerUnreachableException>() | |||
.Or<SocketException>() | |||
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | |||
{ | |||
_logger.LogWarning(ex.ToString()); | |||
}); | |||
using (var channel = _persistentConnection.CreateModel()) | |||
{ | |||
var commandName = command.Name; | |||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); | |||
var message = JsonConvert.SerializeObject(command); | |||
var body = Encoding.UTF8.GetBytes(message); | |||
policy.Execute(() => | |||
{ | |||
channel.BasicPublish(exchange: BROKER_NAME, | |||
routingKey: commandName, | |||
basicProperties: null, | |||
body: body); | |||
}); | |||
} | |||
} | |||
private IModel CreateConsumerChannel() | |||
{ | |||
if (!_persistentConnection.IsConnected) | |||
{ | |||
_persistentConnection.TryConnect(); | |||
} | |||
var channel = _persistentConnection.CreateModel(); | |||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); | |||
_queueName = channel.QueueDeclare().QueueName; | |||
var consumer = new EventingBasicConsumer(channel); | |||
consumer.Received += async (model, ea) => | |||
{ | |||
var commandName = ea.RoutingKey; | |||
var message = Encoding.UTF8.GetString(ea.Body); | |||
await InvokeHandler(commandName, message); | |||
}; | |||
channel.BasicConsume(queue: _queueName, | |||
noAck: true, | |||
consumer: consumer); | |||
channel.CallbackException += (sender, ea) => | |||
{ | |||
_consumerChannel.Dispose(); | |||
_consumerChannel = CreateConsumerChannel(); | |||
}; | |||
return channel; | |||
} | |||
private Task InvokeHandler(string commandName, string message) | |||
{ | |||
if (_handlers.ContainsKey(commandName)) | |||
{ | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
if (_consumerChannel != null) | |||
{ | |||
_consumerChannel.Dispose(); | |||
} | |||
} | |||
} | |||
} | |||
*/ |
@ -0,0 +1,36 @@ | |||
using System; | |||
using System.ComponentModel.DataAnnotations; | |||
namespace eShopOnContainers.Core.Models.Basket | |||
{ | |||
public class BasketCheckout | |||
{ | |||
[Required] | |||
public string City { get; set; } | |||
[Required] | |||
public string Street { get; set; } | |||
[Required] | |||
public string State { get; set; } | |||
[Required] | |||
public string Country { get; set; } | |||
public string ZipCode { get; set; } | |||
[Required] | |||
public string CardNumber { get; set; } | |||
[Required] | |||
public string CardHolderName { get; set; } | |||
[Required] | |||
public DateTime CardExpiration { get; set; } | |||
[Required] | |||
public string CardSecurityNumber { get; set; } | |||
public int CardTypeId { get; set; } | |||
public string Buyer { get; set; } | |||
[Required] | |||
public Guid RequestId { get; set; } | |||
} | |||
} |
@ -1,13 +1,15 @@ | |||
using System.Collections.ObjectModel; | |||
using eShopOnContainers.Core.Models.Basket; | |||
using System.Collections.ObjectModel; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Core.Services.Order | |||
{ | |||
public interface IOrderService | |||
{ | |||
Task CreateOrderAsync(Models.Orders.Order newOrder, string token); | |||
//Task CreateOrderAsync(Models.Orders.Order newOrder, string token); | |||
Task<ObservableCollection<Models.Orders.Order>> GetOrdersAsync(string token); | |||
Task<Models.Orders.Order> GetOrderAsync(int orderId, string token); | |||
Task<ObservableCollection<Models.Orders.CardType>> GetCardTypesAsync(string token); | |||
BasketCheckout MapOrderToBasket(Models.Orders.Order order); | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using Microsoft.eShopOnContainers.Services.Basket.API.Model; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Basket.API.IntegrationEvents.Events | |||
{ | |||
public class UserCheckoutAcceptedIntegrationEvent : IntegrationEvent | |||
{ | |||
public string UserId { get; } | |||
public int OrderNumber { get; set; } | |||
public string City { get; set; } | |||
public string Street { get; set; } | |||
public string State { get; set; } | |||
public string Country { get; set; } | |||
public string ZipCode { get; set; } | |||
public string CardNumber { get; set; } | |||
public string CardHolderName { get; set; } | |||
public DateTime CardExpiration { get; set; } | |||
public string CardSecurityNumber { get; set; } | |||
public int CardTypeId { get; set; } | |||
public string Buyer { get; set; } | |||
public Guid RequestId { get; set; } | |||
public CustomerBasket Basket { get; } | |||
public UserCheckoutAcceptedIntegrationEvent(string userId, 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) | |||
{ | |||
UserId = userId; | |||
City = city; | |||
Street = street; | |||
State = state; | |||
Country = country; | |||
ZipCode = zipCode; | |||
CardNumber = cardNumber; | |||
CardHolderName = cardHolderName; | |||
CardExpiration = cardExpiration; | |||
CardSecurityNumber = cardSecurityNumber; | |||
CardTypeId = cardTypeId; | |||
Buyer = buyer; | |||
Basket = basket; | |||
RequestId = requestId; | |||
} | |||
} | |||
} |
@ -0,0 +1,35 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Basket.API.Model | |||
{ | |||
public class BasketCheckout | |||
{ | |||
public string City { get; set; } | |||
public string Street { get; set; } | |||
public string State { get; set; } | |||
public string Country { get; set; } | |||
public string ZipCode { get; set; } | |||
public string CardNumber { get; set; } | |||
public string CardHolderName { get; set; } | |||
public DateTime CardExpiration { get; set; } | |||
public string CardSecurityNumber { get; set; } | |||
public int CardTypeId { get; set; } | |||
public string Buyer { get; set; } | |||
public Guid RequestId { get; set; } | |||
} | |||
} | |||
@ -0,0 +1,12 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services | |||
{ | |||
public interface IIdentityService | |||
{ | |||
string GetUserIdentity(); | |||
} | |||
} |
@ -0,0 +1,24 @@ | |||
| |||
using Microsoft.AspNetCore.Http; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services | |||
{ | |||
public class IdentityService : IIdentityService | |||
{ | |||
private IHttpContextAccessor _context; | |||
public IdentityService(IHttpContextAccessor context) | |||
{ | |||
_context = context ?? throw new ArgumentNullException(nameof(context)); | |||
} | |||
public string GetUserIdentity() | |||
{ | |||
return _context.HttpContext.User.FindFirst("sub").Value; | |||
} | |||
} | |||
} |
@ -0,0 +1,107 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
[DbContext(typeof(CatalogContext))] | |||
[Migration("20170509130025_AddStockProductItem")] | |||
partial class AddStockProductItem | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Brand") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogBrand"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<int>("AvailableStock"); | |||
b.Property<int>("CatalogBrandId"); | |||
b.Property<int>("CatalogTypeId"); | |||
b.Property<string>("Description"); | |||
b.Property<int>("MaxStockThreshold"); | |||
b.Property<string>("Name") | |||
.IsRequired() | |||
.HasMaxLength(50); | |||
b.Property<bool>("OnReorder"); | |||
b.Property<string>("PictureUri"); | |||
b.Property<decimal>("Price"); | |||
b.Property<int>("RestockThreshold"); | |||
b.HasKey("Id"); | |||
b.HasIndex("CatalogBrandId"); | |||
b.HasIndex("CatalogTypeId"); | |||
b.ToTable("Catalog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Type") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogType"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") | |||
.WithMany() | |||
.HasForeignKey("CatalogBrandId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") | |||
.WithMany() | |||
.HasForeignKey("CatalogTypeId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,55 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
public partial class AddStockProductItem : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.AddColumn<int>( | |||
name: "AvailableStock", | |||
table: "Catalog", | |||
nullable: false, | |||
defaultValue: 0); | |||
migrationBuilder.AddColumn<int>( | |||
name: "MaxStockThreshold", | |||
table: "Catalog", | |||
nullable: false, | |||
defaultValue: 0); | |||
migrationBuilder.AddColumn<bool>( | |||
name: "OnReorder", | |||
table: "Catalog", | |||
nullable: false, | |||
defaultValue: false); | |||
migrationBuilder.AddColumn<int>( | |||
name: "RestockThreshold", | |||
table: "Catalog", | |||
nullable: false, | |||
defaultValue: 0); | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropColumn( | |||
name: "AvailableStock", | |||
table: "Catalog"); | |||
migrationBuilder.DropColumn( | |||
name: "MaxStockThreshold", | |||
table: "Catalog"); | |||
migrationBuilder.DropColumn( | |||
name: "OnReorder", | |||
table: "Catalog"); | |||
migrationBuilder.DropColumn( | |||
name: "RestockThreshold", | |||
table: "Catalog"); | |||
} | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling | |||
{ | |||
using BuildingBlocks.EventBus.Abstractions; | |||
using System.Threading.Tasks; | |||
using BuildingBlocks.EventBus.Events; | |||
using Infrastructure; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using global::Catalog.API.IntegrationEvents; | |||
using IntegrationEvents.Events; | |||
public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : | |||
IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent> | |||
{ | |||
private readonly CatalogContext _catalogContext; | |||
private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | |||
public OrderStatusChangedToAwaitingValidationIntegrationEventHandler(CatalogContext catalogContext, | |||
ICatalogIntegrationEventService catalogIntegrationEventService) | |||
{ | |||
_catalogContext = catalogContext; | |||
_catalogIntegrationEventService = catalogIntegrationEventService; | |||
} | |||
public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent command) | |||
{ | |||
var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); | |||
foreach (var orderStockItem in command.OrderStockItems) | |||
{ | |||
var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | |||
var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; | |||
var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); | |||
confirmedOrderStockItems.Add(confirmedOrderStockItem); | |||
} | |||
var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) | |||
? (IntegrationEvent) new OrderStockRejectedIntegrationEvent(command.OrderId, confirmedOrderStockItems) | |||
: new OrderStockConfirmedIntegrationEvent(command.OrderId); | |||
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); | |||
await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); | |||
} | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling | |||
{ | |||
using BuildingBlocks.EventBus.Abstractions; | |||
using System.Threading.Tasks; | |||
using Infrastructure; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | |||
public class OrderStatusChangedToPaidIntegrationEventHandler : | |||
IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent> | |||
{ | |||
private readonly CatalogContext _catalogContext; | |||
public OrderStatusChangedToPaidIntegrationEventHandler(CatalogContext catalogContext) | |||
{ | |||
_catalogContext = catalogContext; | |||
} | |||
public async Task Handle(OrderStatusChangedToPaidIntegrationEvent command) | |||
{ | |||
//we're not blocking stock/inventory | |||
foreach (var orderStockItem in command.OrderStockItems) | |||
{ | |||
var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | |||
catalogItem.RemoveStock(orderStockItem.Units); | |||
} | |||
await _catalogContext.SaveChangesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,30 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
using BuildingBlocks.EventBus.Events; | |||
using System.Collections.Generic; | |||
public class OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public class OrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public int Units { get; } | |||
public OrderStockItem(int productId, int units) | |||
{ | |||
ProductId = productId; | |||
Units = units; | |||
} | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
using System.Collections.Generic; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
public OrderStatusChangedToPaidIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
using BuildingBlocks.EventBus.Events; | |||
public class OrderStockConfirmedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
using BuildingBlocks.EventBus.Events; | |||
using System.Collections.Generic; | |||
public class OrderStockRejectedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public List<ConfirmedOrderStockItem> OrderStockItems { get; } | |||
public OrderStockRejectedIntegrationEvent(int orderId, | |||
List<ConfirmedOrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public class ConfirmedOrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public bool HasStock { get; } | |||
public ConfirmedOrderStockItem(int productId, bool hasStock) | |||
{ | |||
ProductId = productId; | |||
HasStock = hasStock; | |||
} | |||
} | |||
} |
@ -0,0 +1,3 @@ | |||
* | |||
!obj/Docker/publish/* | |||
!obj/Docker/empty/ |
@ -0,0 +1,5 @@ | |||
FROM microsoft/aspnetcore:1.1.2 | |||
ARG source | |||
WORKDIR /app | |||
COPY ${source:-obj/Docker/publish} . | |||
ENTRYPOINT ["dotnet", "GracePeriodManager.dll"] |
@ -0,0 +1,36 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<OutputType>Exe</OutputType> | |||
<TargetFramework>netcoreapp1.1</TargetFramework> | |||
<RuntimeFrameworkVersion>1.1.2</RuntimeFrameworkVersion> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.1.0" /> | |||
<PackageReference Include="Dapper" Version="1.50.2" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.0" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Options" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" /> | |||
<ProjectReference Include="..\..\Ordering\Ordering.Infrastructure\Ordering.Infrastructure.csproj" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Update=".dockerignore"> | |||
<DependentUpon>Dockerfile</DependentUpon> | |||
</None> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,11 @@ | |||
namespace GracePeriodManager.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class GracePeriodConfirmedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get;} | |||
public GracePeriodConfirmedIntegrationEvent(int orderId) => OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
namespace GracePeriodManager | |||
{ | |||
public class ManagerSettings | |||
{ | |||
public string ConnectionString { get; set; } | |||
public string EventBusConnection { get; set; } | |||
public int GracePeriodTime { get; set; } | |||
public int CheckUpdateTime { get; set; } | |||
} | |||
} |
@ -0,0 +1,94 @@ | |||
namespace GracePeriodManager | |||
{ | |||
using System.IO; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Autofac.Extensions.DependencyInjection; | |||
using Autofac; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||
using Microsoft.Extensions.Logging; | |||
using Microsoft.Extensions.Options; | |||
using RabbitMQ.Client; | |||
using Services; | |||
public class Program | |||
{ | |||
public static IConfigurationRoot Configuration { get; set; } | |||
public static void Main(string[] args) => MainAsync().Wait(); | |||
static async Task MainAsync() | |||
{ | |||
StartUp(); | |||
IServiceCollection services = new ServiceCollection(); | |||
var serviceProvider = ConfigureServices(services); | |||
var logger = serviceProvider.GetService<ILoggerFactory>(); | |||
Configure(logger); | |||
var gracePeriodManagerService = serviceProvider | |||
.GetRequiredService<IManagerService>(); | |||
var checkUpdateTime = serviceProvider | |||
.GetRequiredService<IOptions<ManagerSettings>>().Value.CheckUpdateTime; | |||
while (true) | |||
{ | |||
gracePeriodManagerService.CheckConfirmedGracePeriodOrders(); | |||
await Task.Delay(checkUpdateTime); | |||
} | |||
} | |||
public static void StartUp() | |||
{ | |||
var builder = new ConfigurationBuilder() | |||
.SetBasePath(Directory.GetCurrentDirectory()) | |||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | |||
.AddEnvironmentVariables(); | |||
Configuration = builder.Build(); | |||
} | |||
public static IServiceProvider ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddLogging() | |||
.AddOptions() | |||
.Configure<ManagerSettings>(Configuration) | |||
.AddSingleton<IManagerService, ManagerService>() | |||
.AddSingleton<IRabbitMQPersistentConnection>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<ManagerSettings>>().Value; | |||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | |||
var factory = new ConnectionFactory() | |||
{ | |||
HostName = settings.EventBusConnection | |||
}; | |||
return new DefaultRabbitMQPersistentConnection(factory, logger); | |||
}); | |||
RegisterEventBus(services); | |||
var container = new ContainerBuilder(); | |||
container.Populate(services); | |||
return new AutofacServiceProvider(container.Build()); | |||
} | |||
public static void Configure(ILoggerFactory loggerFactory) | |||
{ | |||
loggerFactory | |||
.AddConsole(Configuration.GetSection("Logging")) | |||
.AddConsole(LogLevel.Debug); | |||
} | |||
private static void RegisterEventBus(IServiceCollection services) | |||
{ | |||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(); | |||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | |||
} | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
namespace GracePeriodManager.Services | |||
{ | |||
public interface IManagerService | |||
{ | |||
void CheckConfirmedGracePeriodOrders(); | |||
} | |||
} |
@ -0,0 +1,62 @@ | |||
namespace GracePeriodManager.Services | |||
{ | |||
using Dapper; | |||
using GracePeriodManager.IntegrationEvents.Events; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.Extensions.Logging; | |||
using Microsoft.Extensions.Options; | |||
using System.Collections.Generic; | |||
using System.Data.SqlClient; | |||
public class ManagerService : IManagerService | |||
{ | |||
private readonly ManagerSettings _settings; | |||
private readonly IEventBus _eventBus; | |||
private readonly ILogger<ManagerService> _logger; | |||
public ManagerService(IOptions<ManagerSettings> settings, | |||
IEventBus eventBus, | |||
ILogger<ManagerService> logger) | |||
{ | |||
_settings = settings.Value; | |||
_eventBus = eventBus; | |||
_logger = logger; | |||
} | |||
public void CheckConfirmedGracePeriodOrders() | |||
{ | |||
var orderIds = GetConfirmedGracePeriodOrders(); | |||
foreach (var orderId in orderIds) | |||
{ | |||
var confirmGracePeriodEvent = new GracePeriodConfirmedIntegrationEvent(orderId); | |||
_eventBus.Publish(confirmGracePeriodEvent); | |||
} | |||
} | |||
private IEnumerable<int> GetConfirmedGracePeriodOrders() | |||
{ | |||
IEnumerable<int> orderIds = new List<int>(); | |||
using (var conn = new SqlConnection(_settings.ConnectionString)) | |||
{ | |||
try | |||
{ | |||
_logger.LogInformation("Grace Period Manager Client is trying to connect to database server"); | |||
conn.Open(); | |||
orderIds = conn.Query<int>( | |||
@"SELECT Id FROM [Microsoft.eShopOnContainers.Services.OrderingDb].[ordering].[orders] | |||
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime | |||
AND [OrderStatusId] = 1", | |||
new { GracePeriodTime = _settings.GracePeriodTime }); | |||
} | |||
catch (SqlException exception) | |||
{ | |||
_logger.LogCritical($"FATAL ERROR: Database connections could not be opened: {exception.Message}"); | |||
} | |||
} | |||
return orderIds; | |||
} | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;", | |||
"GracePeriodTime": "1", | |||
"CheckUpdateTime": "30000" | |||
} |
@ -0,0 +1,21 @@ | |||
using MediatR; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Runtime.Serialization; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Commands | |||
{ | |||
public class CancelOrderCommand : IRequest<bool> | |||
{ | |||
[DataMember] | |||
public int OrderNumber { get; private set; } | |||
public CancelOrderCommand(int orderNumber) | |||
{ | |||
OrderNumber = orderNumber; | |||
} | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Commands | |||
{ | |||
public class CancelOrderCommandIdentifiedHandler : IdentifierCommandHandler<CancelOrderCommand, bool> | |||
{ | |||
public CancelOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) | |||
{ | |||
} | |||
protected override bool CreateResultForDuplicateRequest() | |||
{ | |||
return true; // Ignore duplicate requests for processing order. | |||
} | |||
} | |||
public class CancelOrderCommandHandler : IAsyncRequestHandler<CancelOrderCommand, bool> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public CancelOrderCommandHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
/// <summary> | |||
/// Handler which processes the command when | |||
/// customer executes cancel order from app | |||
/// </summary> | |||
/// <param name="command"></param> | |||
/// <returns></returns> | |||
public async Task<bool> Handle(CancelOrderCommand command) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber); | |||
orderToUpdate.SetCancelledStatus(); | |||
return await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
using MediatR; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Runtime.Serialization; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Commands | |||
{ | |||
public class ShipOrderCommand : IRequest<bool> | |||
{ | |||
[DataMember] | |||
public int OrderNumber { get; private set; } | |||
public ShipOrderCommand(int orderNumber) | |||
{ | |||
OrderNumber = orderNumber; | |||
} | |||
} | |||
} |
@ -0,0 +1,43 @@ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Commands | |||
{ | |||
public class ShipOrderCommandIdentifiedHandler : IdentifierCommandHandler<ShipOrderCommand, bool> | |||
{ | |||
public ShipOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) | |||
{ | |||
} | |||
protected override bool CreateResultForDuplicateRequest() | |||
{ | |||
return true; // Ignore duplicate requests for processing order. | |||
} | |||
} | |||
public class ShipOrderCommandHandler : IAsyncRequestHandler<ShipOrderCommand, bool> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public ShipOrderCommandHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
/// <summary> | |||
/// Handler which processes the command when | |||
/// administrator executes ship order from app | |||
/// </summary> | |||
/// <param name="command"></param> | |||
/// <returns></returns> | |||
public async Task<bool> Handle(ShipOrderCommand command) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber); | |||
orderToUpdate.SetShippedStatus(); | |||
return await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,43 @@ | |||
namespace Ordering.API.Application.DomainEventHandlers.OrderGracePeriodConfirmed | |||
{ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.Extensions.Logging; | |||
using Domain.Events; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Ordering.API.Application.IntegrationEvents; | |||
using System.Linq; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
public class OrderStatusChangedToAwaitingValidationDomainEventHandler | |||
: IAsyncNotificationHandler<OrderStatusChangedToAwaitingValidationDomainEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
private readonly ILoggerFactory _logger; | |||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; | |||
public OrderStatusChangedToAwaitingValidationDomainEventHandler( | |||
IOrderRepository orderRepository, ILoggerFactory logger, | |||
IOrderingIntegrationEventService orderingIntegrationEventService) | |||
{ | |||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); | |||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||
_orderingIntegrationEventService = orderingIntegrationEventService; | |||
} | |||
public async Task Handle(OrderStatusChangedToAwaitingValidationDomainEvent orderStatusChangedToAwaitingValidationDomainEvent) | |||
{ | |||
_logger.CreateLogger(nameof(OrderStatusChangedToAwaitingValidationDomainEvent)) | |||
.LogTrace($"Order with Id: {orderStatusChangedToAwaitingValidationDomainEvent.OrderId} has been successfully updated with " + | |||
$"a status order id: {OrderStatus.AwaitingValidation.Id}"); | |||
var orderStockList = orderStatusChangedToAwaitingValidationDomainEvent.OrderItems | |||
.Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); | |||
var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent( | |||
orderStatusChangedToAwaitingValidationDomainEvent.OrderId, orderStockList); | |||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToAwaitingValidationIntegrationEvent); | |||
} | |||
} | |||
} |
@ -0,0 +1,43 @@ | |||
namespace Ordering.API.Application.DomainEventHandlers.OrderPaid | |||
{ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.Extensions.Logging; | |||
using Domain.Events; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Ordering.API.Application.IntegrationEvents; | |||
using System.Linq; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
public class OrderStatusChangedToPaidDomainEventHandler | |||
: IAsyncNotificationHandler<OrderStatusChangedToPaidDomainEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
private readonly ILoggerFactory _logger; | |||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; | |||
public OrderStatusChangedToPaidDomainEventHandler( | |||
IOrderRepository orderRepository, ILoggerFactory logger, | |||
IOrderingIntegrationEventService orderingIntegrationEventService) | |||
{ | |||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); | |||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||
_orderingIntegrationEventService = orderingIntegrationEventService; | |||
} | |||
public async Task Handle(OrderStatusChangedToPaidDomainEvent orderStatusChangedToPaidDomainEvent) | |||
{ | |||
_logger.CreateLogger(nameof(OrderStatusChangedToPaidDomainEventHandler)) | |||
.LogTrace($"Order with Id: {orderStatusChangedToPaidDomainEvent.OrderId} has been successfully updated with " + | |||
$"a status order id: {OrderStatus.Paid.Id}"); | |||
var orderStockList = orderStatusChangedToPaidDomainEvent.OrderItems | |||
.Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); | |||
var orderStatusChangedToPaidIntegrationEvent = new OrderStatusChangedToPaidIntegrationEvent(orderStatusChangedToPaidDomainEvent.OrderId, | |||
orderStockList); | |||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToPaidIntegrationEvent); | |||
} | |||
} | |||
} |
@ -0,0 +1,38 @@ | |||
namespace Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed | |||
{ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.Extensions.Logging; | |||
using Domain.Events; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Ordering.API.Application.IntegrationEvents; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
public class OrderStatusChangedToStockConfirmedDomainEventHandler | |||
: IAsyncNotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
private readonly ILoggerFactory _logger; | |||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; | |||
public OrderStatusChangedToStockConfirmedDomainEventHandler( | |||
IOrderRepository orderRepository, ILoggerFactory logger, | |||
IOrderingIntegrationEventService orderingIntegrationEventService) | |||
{ | |||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); | |||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||
_orderingIntegrationEventService = orderingIntegrationEventService; | |||
} | |||
public async Task Handle(OrderStatusChangedToStockConfirmedDomainEvent orderStatusChangedToStockConfirmedDomainEvent) | |||
{ | |||
_logger.CreateLogger(nameof(OrderStatusChangedToStockConfirmedDomainEventHandler)) | |||
.LogTrace($"Order with Id: {orderStatusChangedToStockConfirmedDomainEvent.OrderId} has been successfully updated with " + | |||
$"a status order id: {OrderStatus.StockConfirmed.Id}"); | |||
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(orderStatusChangedToStockConfirmedDomainEvent.OrderId); | |||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToStockConfirmedIntegrationEvent); | |||
} | |||
} | |||
} |
@ -0,0 +1,32 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
public class GracePeriodConfirmedIntegrationEventHandler : IIntegrationEventHandler<GracePeriodConfirmedIntegrationEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public GracePeriodConfirmedIntegrationEventHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
/// <summary> | |||
/// Event handler which confirms that the grace period | |||
/// has been completed and order will not initially be cancelled. | |||
/// Therefore, the order process continues for validation. | |||
/// </summary> | |||
/// <param name="event"> | |||
/// </param> | |||
/// <returns></returns> | |||
public async Task Handle(GracePeriodConfirmedIntegrationEvent @event) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); | |||
orderToUpdate.SetAwaitingValidationStatus(); | |||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
using System.Threading.Tasks; | |||
public class OrderPaymentFailedIntegrationEventHandler : | |||
IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public OrderPaymentFailedIntegrationEventHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
public async Task Handle(OrderPaymentFailedIntegrationEvent @event) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); | |||
orderToUpdate.SetCancelledStatus(); | |||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
using System.Threading.Tasks; | |||
public class OrderPaymentSuccededIntegrationEventHandler : | |||
IIntegrationEventHandler<OrderPaymentSuccededIntegrationEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public OrderPaymentSuccededIntegrationEventHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
public async Task Handle(OrderPaymentSuccededIntegrationEvent @event) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); | |||
orderToUpdate.SetPaidStatus(); | |||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using System.Threading.Tasks; | |||
using Events; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
public class OrderStockConfirmedIntegrationEventHandler : | |||
IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public OrderStockConfirmedIntegrationEventHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
public async Task Handle(OrderStockConfirmedIntegrationEvent @event) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); | |||
orderToUpdate.SetStockConfirmedStatus(); | |||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using System.Threading.Tasks; | |||
using Events; | |||
using System.Linq; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
public class OrderStockRejectedIntegrationEventHandler : IIntegrationEventHandler<OrderStockRejectedIntegrationEvent> | |||
{ | |||
private readonly IOrderRepository _orderRepository; | |||
public OrderStockRejectedIntegrationEventHandler(IOrderRepository orderRepository) | |||
{ | |||
_orderRepository = orderRepository; | |||
} | |||
public async Task Handle(OrderStockRejectedIntegrationEvent @event) | |||
{ | |||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); | |||
var orderStockRejectedItems = @event.OrderStockItems | |||
.FindAll(c => !c.HasStock) | |||
.Select(c => c.ProductId); | |||
orderToUpdate.SetCancelledStatusWhenStockIsRejected(orderStockRejectedItems); | |||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,58 @@ | |||
using System; | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using System.Threading.Tasks; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.Extensions.Logging; | |||
using Ordering.API.Application.IntegrationEvents.Events; | |||
namespace Ordering.API.Application.IntegrationEvents.EventHandling | |||
{ | |||
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent> | |||
{ | |||
private readonly IMediator _mediator; | |||
private readonly ILoggerFactory _logger; | |||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; | |||
public UserCheckoutAcceptedIntegrationEventHandler(IMediator mediator, | |||
ILoggerFactory logger, IOrderingIntegrationEventService orderingIntegrationEventService) | |||
{ | |||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); | |||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); | |||
} | |||
/// <summary> | |||
/// Integration event handler which starts the create order process | |||
/// </summary> | |||
/// <param name="eventMsg"> | |||
/// Integration event message which is sent by the | |||
/// basket.api once it has successfully process the | |||
/// order items. | |||
/// </param> | |||
/// <returns></returns> | |||
public async Task Handle(UserCheckoutAcceptedIntegrationEvent eventMsg) | |||
{ | |||
var result = false; | |||
// Send Integration event to clean basket once basket is converted to Order and before starting with the order creation process | |||
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(eventMsg.UserId); | |||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStartedIntegrationEvent); | |||
if (eventMsg.RequestId != Guid.Empty) | |||
{ | |||
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items, eventMsg.UserId, eventMsg.City, eventMsg.Street, | |||
eventMsg.State, eventMsg.Country, eventMsg.ZipCode, | |||
eventMsg.CardNumber, eventMsg.CardHolderName, eventMsg.CardExpiration, | |||
eventMsg.CardSecurityNumber, eventMsg.CardTypeId); | |||
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, eventMsg.RequestId); | |||
result = await _mediator.Send(requestCreateOrder); | |||
} | |||
_logger.CreateLogger(nameof(UserCheckoutAcceptedIntegrationEventHandler)) | |||
.LogTrace(result ? $"UserCheckoutAccepted integration event has been received and a create new order process is started with requestId: {eventMsg.RequestId}" : | |||
$"UserCheckoutAccepted integration event has been received but a new order process has failed with requestId: {eventMsg.RequestId}"); | |||
} | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class GracePeriodConfirmedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public GracePeriodConfirmedIntegrationEvent(int orderId) => | |||
OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderPaymentFailedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderPaymentFailedIntegrationEvent(int orderId) => OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderPaymentSuccededIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderPaymentSuccededIntegrationEvent(int orderId) => OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,30 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using System.Collections.Generic; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public class OrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public int Units { get; } | |||
public OrderStockItem(int productId, int units) | |||
{ | |||
ProductId = productId; | |||
Units = units; | |||
} | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using System.Collections.Generic; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
public OrderStatusChangedToPaidIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderStatusChangedToStockConfirmedIntegrationEvent(int orderId) | |||
=> OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
public class OrderStockConfirmedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System.Collections.Generic; | |||
public class OrderStockRejectedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public List<ConfirmedOrderStockItem> OrderStockItems { get; } | |||
public OrderStockRejectedIntegrationEvent(int orderId, | |||
List<ConfirmedOrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public class ConfirmedOrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public bool HasStock { get; } | |||
public ConfirmedOrderStockItem(int productId, bool hasStock) | |||
{ | |||
ProductId = productId; | |||
HasStock = hasStock; | |||
} | |||
} | |||
} |
@ -0,0 +1,59 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using Ordering.API.Application.Models; | |||
using System; | |||
namespace Ordering.API.Application.IntegrationEvents.Events | |||
{ | |||
public class UserCheckoutAcceptedIntegrationEvent : IntegrationEvent | |||
{ | |||
public string UserId { get; } | |||
public string City { get; set; } | |||
public string Street { get; set; } | |||
public string State { get; set; } | |||
public string Country { get; set; } | |||
public string ZipCode { get; set; } | |||
public string CardNumber { get; set; } | |||
public string CardHolderName { get; set; } | |||
public DateTime CardExpiration { get; set; } | |||
public string CardSecurityNumber { get; set; } | |||
public int CardTypeId { get; set; } | |||
public string Buyer { get; set; } | |||
public Guid RequestId { get; set; } | |||
public CustomerBasket Basket { get; } | |||
public UserCheckoutAcceptedIntegrationEvent(string userId, 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) | |||
{ | |||
UserId = userId; | |||
City = city; | |||
Street = street; | |||
State = state; | |||
Country = country; | |||
ZipCode = zipCode; | |||
CardNumber = cardNumber; | |||
CardHolderName = cardHolderName; | |||
CardExpiration = cardExpiration; | |||
CardSecurityNumber = cardSecurityNumber; | |||
CardTypeId = cardTypeId; | |||
Buyer = buyer; | |||
Basket = basket; | |||
RequestId = requestId; | |||
} | |||
} | |||
} |
@ -1,14 +1,10 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.IntegrationEvents | |||
namespace Ordering.API.Application.IntegrationEvents | |||
{ | |||
public interface IOrderingIntegrationEventService | |||
{ | |||
Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt); | |||
Task PublishThroughEventBusAsync(IntegrationEvent evt); | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Models | |||
{ | |||
public class BasketItem | |||
{ | |||
public string Id { get; set; } | |||
public string ProductId { get; set; } | |||
public string ProductName { get; set; } | |||
public decimal UnitPrice { get; set; } | |||
public decimal OldUnitPrice { get; set; } | |||
public int Quantity { get; set; } | |||
public string PictureUrl { get; set; } | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Models | |||
{ | |||
public class CustomerBasket | |||
{ | |||
public string BuyerId { get; set; } | |||
public List<BasketItem> Items { get; set; } | |||
public CustomerBasket(string customerId) | |||
{ | |||
BuyerId = customerId; | |||
Items = new List<BasketItem>(); | |||
} | |||
} | |||
} |
@ -0,0 +1,17 @@ | |||
using FluentValidation; | |||
using Ordering.API.Application.Commands; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Validations | |||
{ | |||
public class CancelOrderCommandValidator : AbstractValidator<CancelOrderCommand> | |||
{ | |||
public CancelOrderCommandValidator() | |||
{ | |||
RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); | |||
} | |||
} | |||
} |
@ -0,0 +1,17 @@ | |||
using FluentValidation; | |||
using Ordering.API.Application.Commands; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Validations | |||
{ | |||
public class ShipOrderCommandValidator : AbstractValidator<ShipOrderCommand> | |||
{ | |||
public ShipOrderCommandValidator() | |||
{ | |||
RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); | |||
} | |||
} | |||
} |