Skip to content

Commit

Permalink
Added Order aggregate
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Oct 7, 2020
1 parent 0b29171 commit 9e6c86b
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 20 deletions.
8 changes: 5 additions & 3 deletions Workshops/PracticalEventSourcing/Carts/Carts/Cart.cs
Expand Up @@ -29,9 +29,6 @@ public class Cart: Aggregate
Guid id,
Guid clientId)
{
Guard.Against.Default(id, nameof(id));
Guard.Against.Default(clientId, nameof(clientId));

var @event = CartInitialized.Create(
id,
clientId,
Expand All @@ -56,6 +53,9 @@ private void Apply(CartInitialized @event)
IProductPriceCalculator productPriceCalculator,
ProductItem productItem)
{
Guard.Against.Null(productPriceCalculator, nameof(productPriceCalculator));
Guard.Against.Null(productItem, nameof(productItem));

if(Status != CartStatus.Pending)
throw new InvalidOperationException($"Adding product for the cart in '{Status}' status is not allowed.");

Expand Down Expand Up @@ -90,6 +90,8 @@ private void Apply(ProductAdded @event)
public void RemoveProduct(
PricedProductItem productItemToBeRemoved)
{
Guard.Against.Null(productItemToBeRemoved, nameof(productItemToBeRemoved));

if(Status != CartStatus.Pending)
throw new InvalidOperationException($"Removing product from the cart in '{Status}' status is not allowed.");

Expand Down
@@ -1,20 +1,38 @@
using System;
using Ardalis.GuardClauses;
using Core.Events;

namespace Orders.Orders.Events
{
public class OrderCancelled: IEvent
{
public Guid OrderId { get; }
public Guid PaymentId { get; }
public Guid? PaymentId { get; }

public DateTime CancelledAt { get; }

public OrderCancelled(Guid orderId, Guid paymentId, DateTime cancelledAt)
public OrderCancelled(
Guid orderId,
Guid? paymentId,
DateTime cancelledAt
)
{
OrderId = orderId;
PaymentId = paymentId;
CancelledAt = cancelledAt;
}

public static OrderCancelled Create(
Guid orderId,
Guid? paymentId,
DateTime cancelledAt
)
{
Guard.Against.Default(orderId, nameof(orderId));
Guard.Against.Default(paymentId, nameof(paymentId));
Guard.Against.Default(cancelledAt, nameof(cancelledAt));

return new OrderCancelled(orderId, paymentId, cancelledAt);
}
}
}
@@ -1,4 +1,5 @@
using System;
using Ardalis.GuardClauses;
using Core.Events;

namespace Orders.Orders.Events
Expand All @@ -8,12 +9,19 @@ public class OrderCompleted : IEvent
public Guid OrderId { get; }

public DateTime CompletedAt { get; }

public OrderCompleted(Guid orderId, DateTime completedAt)
{
OrderId = orderId;
CompletedAt = completedAt;
}

public static OrderCompleted Create(Guid orderId, DateTime completedAt)
{
Guard.Against.Default(orderId, nameof(orderId));
Guard.Against.Default(completedAt, nameof(completedAt));

return new OrderCompleted(orderId, completedAt);
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ardalis.GuardClauses;
using Carts.Carts.ValueObjects;
using Core.Events;

Expand All @@ -16,14 +17,29 @@ public class OrderInitialized: IEvent

public DateTime InitializedAt { get; }

public OrderInitialized(Guid orderId, Guid clientId, IReadOnlyList<PricedProductItem> productItems,
decimal totalPrice, DateTime initializedAt)
private OrderInitialized(
Guid orderId,
Guid clientId,
IReadOnlyList<PricedProductItem> productItems,
decimal totalPrice,
DateTime initializedAt)
{
OrderId = orderId;
ClientId = clientId;
ProductItems = productItems;
TotalPrice = totalPrice;
InitializedAt = initializedAt;
}

public static OrderInitialized Create(Guid orderId, Guid clientId, IReadOnlyList<PricedProductItem> productItems, decimal totalPrice, DateTime initializedAt)
{
Guard.Against.Default(orderId, nameof(orderId));
Guard.Against.Default(clientId, nameof(clientId));
Guard.Against.NullOrEmpty(productItems, nameof(productItems));
Guard.Against.NegativeOrZero(totalPrice, nameof(totalPrice));
Guard.Against.Default(initializedAt, nameof(initializedAt));

return new OrderInitialized(orderId, clientId, productItems, totalPrice, initializedAt);
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ardalis.GuardClauses;
using Carts.Carts.ValueObjects;
using Core.Events;

Expand All @@ -17,14 +18,40 @@ public class OrderPaymentRecorded: IEvent

public DateTime PaymentRecordedAt { get; }

public OrderPaymentRecorded(Guid orderId, Guid paymentId, IReadOnlyList<PricedProductItem> productItems,
decimal amount, DateTime paymentRecordedAt)
private OrderPaymentRecorded(
Guid orderId,
Guid paymentId,
IReadOnlyList<PricedProductItem> productItems,
decimal amount,
DateTime paymentRecordedAt)
{
OrderId = orderId;
PaymentId = paymentId;
ProductItems = productItems;
Amount = amount;
PaymentRecordedAt = paymentRecordedAt;
}

public static OrderPaymentRecorded Create(
Guid orderId,
Guid paymentId,
IReadOnlyList<PricedProductItem> productItems,
decimal amount,
DateTime recordedAt)
{
Guard.Against.Default(orderId, nameof(orderId));
Guard.Against.Default(paymentId, nameof(paymentId));
Guard.Against.NullOrEmpty(productItems, nameof(productItems));
Guard.Against.NegativeOrZero(amount, nameof(amount));
Guard.Against.Default(recordedAt, nameof(recordedAt));

return new OrderPaymentRecorded(
orderId,
paymentId,
productItems,
amount,
recordedAt
);
}
}
}
109 changes: 108 additions & 1 deletion Workshops/PracticalEventSourcing/Orders/Orders/Order.cs
@@ -1,13 +1,120 @@
using System;
using System.Collections.Generic;
using Ardalis.GuardClauses;
using Carts.Carts.ValueObjects;
using Core.Aggregates;
using Orders.Orders.Enums;
using Orders.Orders.Events;

namespace Orders.Orders
{
public class Order: Aggregate
{
public Guid? ClientId { get; private set; }

public IReadOnlyList<PricedProductItem> ProductItems { get; private set; }
public Guid PaymentId { get; private set; }

public decimal TotalPrice { get; private set; }

public OrderStatus Status { get; private set; }

public Guid? PaymentId { get; private set; }

public static Order Initialize(
Guid clientId,
IReadOnlyList<PricedProductItem> productItems,
decimal totalPrice)
{
var orderId = Guid.NewGuid();

return new Order(
orderId,
clientId,
productItems,
totalPrice
);
}

private Order(Guid id, Guid clientId, IReadOnlyList<PricedProductItem> productItems, decimal totalPrice)
{
var @event = OrderInitialized.Create(
id,
clientId,
productItems,
totalPrice,
DateTime.UtcNow
);

Enqueue(@event);
Apply(@event);
}

private void Apply(OrderInitialized @event)
{
Version++;

Id = @event.OrderId;
ClientId = @event.ClientId;
ProductItems = @event.ProductItems;
Status = OrderStatus.Initialized;
}

public void RecordPayment(Guid paymentId, DateTime recordedAt)
{
var @event = OrderPaymentRecorded.Create(
Id,
paymentId,
ProductItems,
TotalPrice,
recordedAt
);

Enqueue(@event);
Apply(@event);
}

private void Apply(OrderPaymentRecorded @event)
{
Version++;

PaymentId = @event.PaymentId;
Status = OrderStatus.Paid;
}

public void Complete()
{
if(Status != OrderStatus.Paid)
throw new InvalidOperationException($"Cannot complete a not paid order.");

var @event = OrderCompleted.Create(Id, DateTime.UtcNow);

Enqueue(@event);
Apply(@event);
}

private void Apply(OrderCompleted @event)
{
Version++;

Status = OrderStatus.Completed;
}

public void Cancel(OrderCancellationReason cancellationReason)
{
if(OrderStatus.Closed.HasFlag(Status))
throw new InvalidOperationException($"Cannot cancel a closed order.");

var @event = OrderCancelled.Create(Id, PaymentId, DateTime.UtcNow);

Enqueue(@event);
Apply(@event);
}

private void Apply(OrderCancelled @event)
{
Version++;

Status = OrderStatus.Cancelled;
}
}
}
@@ -0,0 +1,59 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Core.Commands;
using Core.Repositories;
using MediatR;
using Orders.Orders.Commands;

namespace Orders.Orders
{
public class OrderCommandHandler :
ICommandHandler<InitOrder>,
ICommandHandler<RecordOrderPayment>,
ICommandHandler<CompleteOrder>,
ICommandHandler<CancelOrder>
{
private readonly IRepository<Order> orderRepository;

public OrderCommandHandler(IRepository<Order> orderRepository)
{
Guard.Against.Null(orderRepository, nameof(orderRepository));

this.orderRepository = orderRepository;
}

public async Task<Unit> Handle(InitOrder command, CancellationToken cancellationToken)
{
var order = Order.Initialize(command.ClientId, command.ProductItems, command.TotalPrice);

await orderRepository.Add(order, cancellationToken);

return Unit.Value;
}

public Task<Unit> Handle(RecordOrderPayment command, CancellationToken cancellationToken)
{
return orderRepository.GetAndUpdate(
command.OrderId,
order => order.RecordPayment(command.PaymentId, command.PaymentRecordedAt),
cancellationToken);
}

public Task<Unit> Handle(CompleteOrder command, CancellationToken cancellationToken)
{
return orderRepository.GetAndUpdate(
command.OrderId,
order => order.Complete(),
cancellationToken);
}

public Task<Unit> Handle(CancelOrder command, CancellationToken cancellationToken)
{
return orderRepository.GetAndUpdate(
command.OrderId,
order => order.Cancel(command.CancellationReason),
cancellationToken);
}
}
}
5 changes: 4 additions & 1 deletion Workshops/PracticalEventSourcing/Orders/Orders/OrderSaga.cs
Expand Up @@ -59,7 +59,10 @@ public Task Handle(ProductWasOutOfStock @event, CancellationToken cancellationTo

public Task Handle(OrderCancelled @event, CancellationToken cancellationToken)
{
return SendCommand(new DiscardPayment(@event.PaymentId, DiscardReason.OrderCancelled));
if (!@event.PaymentId.HasValue)
return Task.CompletedTask;

return SendCommand(new DiscardPayment(@event.PaymentId.Value, DiscardReason.OrderCancelled));
}

private static Task SendCommand(ICommand command)
Expand Down
9 changes: 5 additions & 4 deletions Workshops/PracticalEventSourcing/Orders/Orders/OrderStatus.cs
Expand Up @@ -2,9 +2,10 @@ namespace Orders.Orders
{
public enum OrderStatus
{
Initialized,
Completed,
Paid,
Cancelled
Initialized = 1,
Paid = 2,
Completed = 4,
Cancelled = 8,
Closed = Completed | Cancelled
}
}

0 comments on commit 9e6c86b

Please sign in to comment.