@ -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; | using System.Threading.Tasks; | ||||
namespace eShopOnContainers.Core.Services.Order | namespace eShopOnContainers.Core.Services.Order | ||||
{ | { | ||||
public interface IOrderService | 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<ObservableCollection<Models.Orders.Order>> GetOrdersAsync(string token); | ||||
Task<Models.Orders.Order> GetOrderAsync(int orderId, string token); | Task<Models.Orders.Order> GetOrderAsync(int orderId, string token); | ||||
Task<ObservableCollection<Models.Orders.CardType>> GetCardTypesAsync(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 Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Ordering.API.IntegrationEvents | |||||
namespace Ordering.API.Application.IntegrationEvents | |||||
{ | { | ||||
public interface IOrderingIntegrationEventService | public interface IOrderingIntegrationEventService | ||||
{ | { | ||||
Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt); | |||||
Task PublishThroughEventBusAsync(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"); | |||||
} | |||||
} | |||||
} |