Skip to content

Commit

Permalink
Updated Business Logic exercise, to make it more gradual and easier t…
Browse files Browse the repository at this point in the history
…o fill gaps

Now, there's a template where to put business logic without the need to modify unit tests
  • Loading branch information
oskardudycz committed Apr 30, 2024
1 parent 7cb33aa commit b82d7c6
Show file tree
Hide file tree
Showing 39 changed files with 1,253 additions and 1,004 deletions.
Expand Up @@ -24,9 +24,9 @@ public class CartAbandonmentRate
ShoppingCartAbandoned @event,
CancellationToken ct
) =>
aggregateStream(When, ShoppingCart.ToStreamId(@event.ShoppingCartId), ct)!;
aggregateStream(Evolve, ShoppingCart.ToStreamId(@event.ShoppingCartId), ct)!;

public static CartAbandonmentRateCalculated When(CartAbandonmentRateCalculated? lastEvent, object @event) =>
public static CartAbandonmentRateCalculated Evolve(CartAbandonmentRateCalculated? lastEvent, object @event) =>
@event switch
{
ShoppingCartInitialized (var cartId, var clientId, var initializedAt) =>
Expand Down
Expand Up @@ -22,7 +22,7 @@ CancellationToken ct
)
{
var productItems = await aggregateStream(
When,
Evolve,
ShoppingCart.ToStreamId(@event.ShoppingCartId),
ct
);
Expand All @@ -39,7 +39,7 @@ CancellationToken ct
.ToList();
}

public static Dictionary<Guid, int> When(Dictionary<Guid, int>? productItems, object @event) =>
public static Dictionary<Guid, int> Evolve(Dictionary<Guid, int>? productItems, object @event) =>
@event switch
{
ShoppingCartInitialized =>
Expand Down
Expand Up @@ -20,7 +20,7 @@ public static class Configuration
services
.For<ShoppingCart>(
ShoppingCart.Default,
ShoppingCart.When,
ShoppingCart.Evolve,
builder => builder
.AddOn<OpenShoppingCart>(
OpenShoppingCart.Handle,
Expand Down
Expand Up @@ -47,7 +47,7 @@ public record ShoppingCart(
{
public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status);

public static ShoppingCart When(ShoppingCart entity, object @event)
public static ShoppingCart Evolve(ShoppingCart entity, object @event)
{
return @event switch
{
Expand Down
Expand Up @@ -95,7 +95,7 @@ public void ShouldGetCurrentShoppingCartState()
)
};

var shoppingCart = events.Aggregate(ShoppingCart.Default, ShoppingCart.When);
var shoppingCart = events.Aggregate(ShoppingCart.Default, ShoppingCart.Evolve);

shoppingCart.Id.Should().Be(shoppingCartId);
shoppingCart.ClientId.Should().Be(clientId);
Expand Down
Expand Up @@ -220,18 +220,12 @@ public record ShoppingCart(
Dictionary<ProductId, Quantity> ProductItems
)
{
public static ShoppingCart When(ShoppingCart entity, object @event)
public static ShoppingCart Evolve(ShoppingCart entity, object @event)
{
return @event switch
{
ShoppingCartOpened (var cartId, var clientId) =>
entity with
{
Id = cartId,
ClientId = clientId,
Status = ShoppingCartStatus.Pending,
ProductItems = new Dictionary<ProductId, Quantity>()
},
new ShoppingCart(cartId, clientId, ShoppingCartStatus.Pending, new Dictionary<ProductId, Quantity>()),

ProductItemAddedToShoppingCart (_, var productItem) =>
entity with { ProductItems = entity.ProductItems.Add(productItem) },
Expand Down
Expand Up @@ -64,16 +64,16 @@ await API.Given()
private static string ValidSKU => $"CC{DateTime.Now.Ticks}";
private const string ValidDescription = "VALID_DESCRIPTION";

public static TheoryData<RegisterProductRequest> ValidRequests =
[
public static TheoryData<RegisterProductRequest> ValidRequests = new()
{
new RegisterProductRequest(ValidSKU, ValidName, ValidDescription),
new RegisterProductRequest(ValidSKU, ValidName, null)
];
};

public static TheoryData<RegisterProductRequest> InvalidRequests =
[
public static TheoryData<RegisterProductRequest> InvalidRequests =new()
{
new RegisterProductRequest(null, ValidName, ValidDescription),
new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription),
new RegisterProductRequest(ValidSKU, null, ValidDescription)
];
};
}
@@ -1,131 +1,16 @@
using FluentAssertions;
using IntroductionToEventSourcing.BusinessLogic.Tools;
using Xunit;

namespace IntroductionToEventSourcing.BusinessLogic.Immutable;
using static ShoppingCartEvent;

// EVENTS
public abstract record ShoppingCartEvent
{
public record ShoppingCartOpened(
Guid ShoppingCartId,
Guid ClientId
): ShoppingCartEvent;

public record ProductItemAddedToShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
): ShoppingCartEvent;

public record ProductItemRemovedFromShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
): ShoppingCartEvent;

public record ShoppingCartConfirmed(
Guid ShoppingCartId,
DateTime ConfirmedAt
): ShoppingCartEvent;

public record ShoppingCartCanceled(
Guid ShoppingCartId,
DateTime CanceledAt
): ShoppingCartEvent;

// This won't allow external inheritance
private ShoppingCartEvent(){}
}

// VALUE OBJECTS
public record PricedProductItem(
Guid ProductId,
int Quantity,
decimal UnitPrice
);

// ENTITY
public record ShoppingCart(
Guid Id,
Guid ClientId,
ShoppingCartStatus Status,
PricedProductItem[] ProductItems,
DateTime? ConfirmedAt = null,
DateTime? CanceledAt = null
)
{
public static ShoppingCart Default() =>
new (default, default, default, []);

public static ShoppingCart When(ShoppingCart shoppingCart, object @event)
{
return @event switch
{
ShoppingCartOpened(var shoppingCartId, var clientId) =>
shoppingCart with
{
Id = shoppingCartId,
ClientId = clientId,
Status = ShoppingCartStatus.Pending
},
ProductItemAddedToShoppingCart(_, var pricedProductItem) =>
shoppingCart with
{
ProductItems = shoppingCart.ProductItems
.Concat(new [] { pricedProductItem })
.GroupBy(pi => pi.ProductId)
.Select(group => group.Count() == 1?
group.First()
: new PricedProductItem(
group.Key,
group.Sum(pi => pi.Quantity),
group.First().UnitPrice
)
)
.ToArray()
},
ProductItemRemovedFromShoppingCart(_, var pricedProductItem) =>
shoppingCart with
{
ProductItems = shoppingCart.ProductItems
.Select(pi => pi.ProductId == pricedProductItem.ProductId?
new PricedProductItem(
pi.ProductId,
pi.Quantity - pricedProductItem.Quantity,
pi.UnitPrice
)
:pi
)
.Where(pi => pi.Quantity > 0)
.ToArray()
},
ShoppingCartConfirmed(_, var confirmedAt) =>
shoppingCart with
{
Status = ShoppingCartStatus.Confirmed,
ConfirmedAt = confirmedAt
},
ShoppingCartCanceled(_, var canceledAt) =>
shoppingCart with
{
Status = ShoppingCartStatus.Canceled,
CanceledAt = canceledAt
},
_ => shoppingCart
};
}
}

public enum ShoppingCartStatus
{
Pending = 1,
Confirmed = 2,
Canceled = 4
}
using static ShoppingCartEvent;
using static ShoppingCartCommand;

public static class ShoppingCartExtensions
{
public static ShoppingCart GetShoppingCart(this IEnumerable<object> events) =>
events.Aggregate(ShoppingCart.Default(), ShoppingCart.When);
public static ShoppingCart GetShoppingCart(this EventStore eventStore, Guid shoppingCartId) =>
eventStore.ReadStream<ShoppingCartEvent>(shoppingCartId).Aggregate(ShoppingCart.Default(), ShoppingCart.Evolve);
}

public class BusinessLogicTests
Expand All @@ -138,30 +23,87 @@ public void RunningSequenceOfBusinessLogic_ShouldGenerateSequenceOfEvents()
var clientId = Guid.NewGuid();
var shoesId = Guid.NewGuid();
var tShirtId = Guid.NewGuid();
var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100);
var pairOfShoes = new PricedProductItem(shoesId, 1, 100);
var tShirt = new PricedProductItem(tShirtId, 1, 50);

// TODO: Fill the events object with results of your business logic
// to be the same as events below
var events = new List<object>
var twoPairsOfShoes = new ProductItem(shoesId, 2);
var pairOfShoes = new ProductItem(shoesId, 1);
var tShirt = new ProductItem(tShirtId, 1);

var shoesPrice = 100;
var tShirtPrice = 50;

var pricedPairOfShoes = new PricedProductItem(shoesId, 1, shoesPrice);
var pricedTShirt = new PricedProductItem(tShirtId, 1, tShirtPrice);

var eventStore = new EventStore();

// Open
ShoppingCartEvent result =
ShoppingCartService.Handle(
new OpenShoppingCart(shoppingCartId, clientId)
);
eventStore.AppendToStream(shoppingCartId, [result]);

// Add Two Pair of Shoes
var shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
result = ShoppingCartService.Handle(
FakeProductPriceCalculator.Returning(shoesPrice),
new AddProductItemToShoppingCart(shoppingCartId, twoPairsOfShoes),
shoppingCart
);
eventStore.AppendToStream(shoppingCartId, [result]);

// Add T-Shirt
shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
result = ShoppingCartService.Handle(
FakeProductPriceCalculator.Returning(tShirtPrice),
new AddProductItemToShoppingCart(shoppingCartId, tShirt),
shoppingCart
);
eventStore.AppendToStream(shoppingCartId, [result]);

// Remove a pair of shoes
shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
result = ShoppingCartService.Handle(
new RemoveProductItemFromShoppingCart(shoppingCartId, pricedPairOfShoes),
shoppingCart
);
eventStore.AppendToStream(shoppingCartId, [result]);

// Confirm
shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
result = ShoppingCartService.Handle(
new ConfirmShoppingCart(shoppingCartId),
shoppingCart
);
eventStore.AppendToStream(shoppingCartId, [result]);

// Try Cancel
var exception = Record.Exception(() =>
{
// new ShoppingCartOpened(shoppingCartId, clientId),
// new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes),
// new ProductItemAddedToShoppingCart(shoppingCartId, tShirt),
// new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes),
// new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow),
// new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow)
};
shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
result = ShoppingCartService.Handle(
new CancelShoppingCart(shoppingCartId),
shoppingCart
);
eventStore.AppendToStream(shoppingCartId, [result]);
});
exception.Should().BeOfType<InvalidOperationException>();

var shoppingCart = events.GetShoppingCart();
shoppingCart = eventStore.GetShoppingCart(shoppingCartId);

shoppingCart.Id.Should().Be(shoppingCartId);
shoppingCart.ClientId.Should().Be(clientId);
shoppingCart.ProductItems.Should().HaveCount(2);
shoppingCart.Status.Should().Be(ShoppingCartStatus.Confirmed);

shoppingCart.ProductItems[0].Should().Be(pairOfShoes);
shoppingCart.ProductItems[1].Should().Be(tShirt);
shoppingCart.ProductItems[0].Should().Be(pricedPairOfShoes);
shoppingCart.ProductItems[1].Should().Be(pricedTShirt);

var events = eventStore.ReadStream<ShoppingCartEvent>(shoppingCartId);
events.Should().HaveCount(5);
events[0].Should().BeOfType<ShoppingCartOpened>();
events[1].Should().BeOfType<ProductItemAddedToShoppingCart>();
events[2].Should().BeOfType<ProductItemAddedToShoppingCart>();
events[3].Should().BeOfType<ProductItemRemovedFromShoppingCart>();
events[4].Should().BeOfType<ShoppingCartConfirmed>();
}
}
@@ -0,0 +1,24 @@
namespace IntroductionToEventSourcing.BusinessLogic.Immutable;

public interface IProductPriceCalculator
{
PricedProductItem Calculate(ProductItem productItems);
}

public class FakeProductPriceCalculator: IProductPriceCalculator
{
private readonly int value;

private FakeProductPriceCalculator(int value)
{
this.value = value;
}

public static FakeProductPriceCalculator Returning(int value) => new(value);

public PricedProductItem Calculate(ProductItem productItem)
{
var (productId, quantity) = productItem;
return new PricedProductItem(productId, quantity, value);
}
}

0 comments on commit b82d7c6

Please sign in to comment.