diff --git a/Directory.Build.props b/Directory.Build.props index 9e7f627..a66f618 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable true enable - 0.15.0-beta.4 + 0.15.0-beta.10 https://github.com/erikshafer/event-sourcing-ecommerce https://event-sourcing.dev/ MIT diff --git a/src/Catalog/Catalog.Api/Catalog.Api.csproj b/src/Catalog/Catalog.Api/Catalog.Api.csproj index 5341711..419355a 100644 --- a/src/Catalog/Catalog.Api/Catalog.Api.csproj +++ b/src/Catalog/Catalog.Api/Catalog.Api.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Catalog/Catalog.Api/Catalog.Api.http b/src/Catalog/Catalog.Api/Catalog.Api.http index 57cc883..65e9cdb 100644 --- a/src/Catalog/Catalog.Api/Catalog.Api.http +++ b/src/Catalog/Catalog.Api/Catalog.Api.http @@ -2,7 +2,7 @@ ### -GET {{Inventory.Api_HostAddress}}/swagger/ +GET {{Inventory.Api_HostAddress}}/api/ Accept: application/json ### diff --git a/src/Catalog/Catalog.Api/Registrations.cs b/src/Catalog/Catalog.Api/Registrations.cs index f864d72..3316528 100644 --- a/src/Catalog/Catalog.Api/Registrations.cs +++ b/src/Catalog/Catalog.Api/Registrations.cs @@ -31,6 +31,9 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration ) ); + // register known event types (e.g. using [EventType] annotation) + TypeMap.RegisterKnownEventTypes(); + // event store (core) services.AddEventStoreClient(configuration["EventStore:ConnectionString"]!); services.AddAggregateStore(); diff --git a/src/Inventory/Inventory.Api/Inventory.Api.csproj b/src/Inventory/Inventory.Api/Inventory.Api.csproj index 5b5f7f5..065a89f 100644 --- a/src/Inventory/Inventory.Api/Inventory.Api.csproj +++ b/src/Inventory/Inventory.Api/Inventory.Api.csproj @@ -7,9 +7,7 @@ - - diff --git a/src/Retail/ShoppingCart.Api/HttpApi/CommandApi.cs b/src/Retail/ShoppingCart.Api/HttpApi/CommandApi.cs index 7a2b3dc..162b4d4 100644 --- a/src/Retail/ShoppingCart.Api/HttpApi/CommandApi.cs +++ b/src/Retail/ShoppingCart.Api/HttpApi/CommandApi.cs @@ -1,25 +1,25 @@ using Eventuous; +using Eventuous.AspNetCore.Web; using Microsoft.AspNetCore.Mvc; using static ShoppingCart.CartCommands.V1; namespace ShoppingCart.Api.HttpApi; [Route("/cart")] -public class CommandApi(IFuncCommandService service) : ControllerBase +public class CommandApi : CommandHttpApiBaseFunc { - [HttpPost] - [Route("open")] - public async Task> OpenCart([FromBody] OpenCart cmd, CancellationToken ct) + private readonly IFuncCommandService _service; + + public CommandApi(IFuncCommandService service) : base(service) { - var result = await service.Handle(cmd, ct); - return Ok(result); + _service = service; } [HttpPost] - [Route("open-with-id")] - public async Task> OpenCart([FromBody] OpenCartWithProvidedId cmd, CancellationToken ct) + [Route("open")] + public async Task> OpenCart([FromBody] OpenCart cmd, CancellationToken ct) { - var result = await service.Handle(cmd, ct); + var result = await _service.Handle(cmd, ct); return Ok(result); } @@ -27,7 +27,7 @@ public async Task> OpenCart([FromBody] OpenCartWithProvided [Route("add-product")] public async Task> OpenCart([FromBody] AddProductToCart cmd, CancellationToken ct) { - var result = await service.Handle(cmd, ct); + var result = await _service.Handle(cmd, ct); return Ok(result); } @@ -35,7 +35,15 @@ public async Task> OpenCart([FromBody] AddProductToCart cmd [Route("remove-product")] public async Task> OpenCart([FromBody] RemoveProductFromCart cmd, CancellationToken ct) { - var result = await service.Handle(cmd, ct); + var result = await _service.Handle(cmd, ct); + return Ok(result); + } + + [HttpPost] + [Route("prepare-checkout")] + public async Task> PrepareForCheckout([FromBody] PrepareCartForCheckout cmd, CancellationToken ct) + { + var result = await _service.Handle(cmd, ct); return Ok(result); } @@ -43,7 +51,7 @@ public async Task> OpenCart([FromBody] RemoveProductFromCar [Route("confirm")] public async Task> OpenCart([FromBody] ConfirmCart cmd, CancellationToken ct) { - var result = await service.Handle(cmd, ct); + var result = await _service.Handle(cmd, ct); return Ok(result); } } diff --git a/src/Retail/ShoppingCart.Api/Program.cs b/src/Retail/ShoppingCart.Api/Program.cs index fb211d4..d67411d 100644 --- a/src/Retail/ShoppingCart.Api/Program.cs +++ b/src/Retail/ShoppingCart.Api/Program.cs @@ -3,6 +3,7 @@ using NodaTime; using NodaTime.Serialization.SystemTextJson; using Serilog; +using ShoppingCart; using ShoppingCart.Api; using ShoppingCart.Api.Infrastructure; diff --git a/src/Retail/ShoppingCart.Api/Registrations.cs b/src/Retail/ShoppingCart.Api/Registrations.cs index 2bd6d90..182633b 100644 --- a/src/Retail/ShoppingCart.Api/Registrations.cs +++ b/src/Retail/ShoppingCart.Api/Registrations.cs @@ -25,6 +25,9 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration ) ); + // register known event types (e.g. using [EventType] annotation) + TypeMap.RegisterKnownEventTypes(); + // event store (core) services.AddEventStoreClient(configuration["EventStore:ConnectionString"]!); services.AddAggregateStore(); diff --git a/src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj b/src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj index 61985dc..edea4fd 100644 --- a/src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj +++ b/src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Retail/ShoppingCart/CartCommands.cs b/src/Retail/ShoppingCart/CartCommands.cs index 86d60fe..f22b5a4 100644 --- a/src/Retail/ShoppingCart/CartCommands.cs +++ b/src/Retail/ShoppingCart/CartCommands.cs @@ -8,11 +8,6 @@ public record OpenCart( string CustomerId ); - public record OpenCartWithProvidedId( - string CartId, - string CustomerId - ); - public record AddProductToCart( string CartId, string ProductId, @@ -25,6 +20,10 @@ public record RemoveProductFromCart( int Quantity ); + public record PrepareCartForCheckout( + string CartId + ); + public record ConfirmCart( string CartId ); diff --git a/src/Retail/ShoppingCart/CartEvents.cs b/src/Retail/ShoppingCart/CartEvents.cs index 677fcc4..ccbfb4c 100644 --- a/src/Retail/ShoppingCart/CartEvents.cs +++ b/src/Retail/ShoppingCart/CartEvents.cs @@ -30,5 +30,10 @@ int Quantity public record CartConfirmed( string CartId ); + + [EventType("V1.EmptyCartDetected")] + public record EmptyCartDetected( + string CartId + ); } } diff --git a/src/Retail/ShoppingCart/CartFuncService.cs b/src/Retail/ShoppingCart/CartFuncService.cs index cc76c32..04ab406 100644 --- a/src/Retail/ShoppingCart/CartFuncService.cs +++ b/src/Retail/ShoppingCart/CartFuncService.cs @@ -14,41 +14,53 @@ public CartFuncService( TypeMapper? typeMap = null) : base(store, typeMap) { - var generatedId = idGenerator.New(); // TODO: leverage + var generatedId = idGenerator.New(); - // Register command handlers OnNew(cmd => GetStream(generatedId), OpenCart); - OnNew(cmd => GetStream(cmd.CartId), OpenCartCommandHasId); - OnExisting(cmd => GetStream(cmd.CartId), AddItemToCart); + OnExisting(cmd => GetStream(cmd.CartId), AddProductToCart); + OnExisting(cmd => GetStream(cmd.CartId), RemoveProductFromCart); + OnExisting(cmd => GetStream(cmd.CartId), PrepareCartForCheckout); - // Helper function to get the stream name from the command static StreamName GetStream(string id) => new($"Cart-{id}"); - // When there's no stream to load, the function only receives the command. (CLOSURES) IEnumerable OpenCart(Commands.OpenCart cmd) { yield return new Events.CartOpened(generatedId, cmd.CustomerId); } - // When there's no stream to load, the function only receives the command - static IEnumerable OpenCartCommandHasId(Commands.OpenCartWithProvidedId cmd) - { - yield return new Events.CartOpened(cmd.CartId, cmd.CustomerId); - } - - // For an existing stream, the function receives the state and the events - static IEnumerable AddItemToCart( + static IEnumerable AddProductToCart( CartState state, object[] originalEvents, Commands.AddProductToCart cmd) { var added = new Events.ProductAddedToCart(cmd.CartId, cmd.ProductId, cmd.Quantity); - yield return added; var newState = state.When(added); + // could have additional logic based on the cart's current state, emmit other events, etc + } - // could have logic based on the cart's current state + static IEnumerable RemoveProductFromCart( + CartState state, + object[] originalEvents, + Commands.RemoveProductFromCart cmd) + { + var removed = new Events.ProductRemovedFromCart(cmd.CartId, cmd.ProductId, cmd.Quantity); + yield return removed; + + var newState = state.When(removed); + + if (newState.HasProductItems is false) + yield return new Events.EmptyCartDetected(cmd.CartId); + } + + static IEnumerable PrepareCartForCheckout( + CartState state, + object[] originalEvents, + Commands.PrepareCartForCheckout cmd) + { + if (state.CanProceedToCheckout()) + yield return new Events.CartConfirmed(cmd.CartId); } } } diff --git a/src/Retail/ShoppingCart/CartState.cs b/src/Retail/ShoppingCart/CartState.cs index 96dc922..26318b1 100644 --- a/src/Retail/ShoppingCart/CartState.cs +++ b/src/Retail/ShoppingCart/CartState.cs @@ -11,6 +11,8 @@ public record CartState : State public CartStatus Status { get; init; } = CartStatus.Unset; public ProductItems ProductItems { get; init; } = null!; + public bool HasProductItems => ProductItems.IsEmpty; + public CartState() { On(Handle); @@ -42,6 +44,12 @@ private static CartState Handle(CartState state, V1.CartOpened @event) => state private static CartState Handle(CartState state, V1.CartConfirmed @event) => state.Status switch { CartStatus.Confirmed => throw InvalidStateChangeException.For(state.Id, CartStatus.Confirmed), - _ => state with { Status = CartStatus.Confirmed } // TODO: make confirm a behavior? but no aggregate? HOW DO!? + _ => state with { Status = CartStatus.Confirmed } + }; + + public bool CanProceedToCheckout() => Status switch + { + CartStatus.Unset or CartStatus.Opened => false, + _ => HasProductItems }; } diff --git a/src/Retail/ShoppingCart/ProductItem.cs b/src/Retail/ShoppingCart/ProductItem.cs index 41909a3..64985b3 100644 --- a/src/Retail/ShoppingCart/ProductItem.cs +++ b/src/Retail/ShoppingCart/ProductItem.cs @@ -46,6 +46,9 @@ public class ProductItems private ProductItems(ProductItem[] values) => Values = values; + public bool IsEmpty => Values.Length == 0; + public int Length => Values.Length; + public ProductItems Add(ProductItem productItem) => new( Values .Concat(new[] { productItem }) @@ -62,4 +65,5 @@ public class ProductItems : pi) .Where(pi => pi.Quantity > 0) .ToArray()); + }