Skip to content

Commit

Permalink
initial cart projection, cleaned up confirm process, cart services
Browse files Browse the repository at this point in the history
  • Loading branch information
erikshafer committed Jun 24, 2024
1 parent 918ce26 commit 8d73b66
Show file tree
Hide file tree
Showing 22 changed files with 195 additions and 32 deletions.
2 changes: 2 additions & 0 deletions src/Catalog/Catalog.Api/Registrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration
.AddEventHandler<OfferStateProjection>()
.WithPartitioningByStream(2));

// TODO: add additional mongo, postgresql, and other custom projections

// services.AddSubscription<AllStreamSubscription, AllStreamSubscriptionOptions>(
// "ProductDraftsProjections",
// builder => builder
Expand Down
8 changes: 0 additions & 8 deletions src/Retail/ShoppingCart.Api/HttpApi/Carts/CartCommandApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ public async Task<ActionResult<Result>> OpenCart([FromBody] CartCommands.V1.Remo
return Ok(result);
}

[HttpPost]
[Route("prepare-checkout")]
public async Task<ActionResult<Result>> PrepareForCheckout([FromBody] CartCommands.V1.ConfirmCartForCheckout cmd, CancellationToken ct)
{
var result = await _service.Handle(cmd, ct);
return Ok(result);
}

[HttpPost]
[Route("confirm")]
public async Task<ActionResult<Result>> OpenCart([FromBody] CartCommands.V1.ConfirmCart cmd, CancellationToken ct)
Expand Down
35 changes: 35 additions & 0 deletions src/Retail/ShoppingCart.Api/Infrastructure/Mongo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using MongoDb.Bson.NodaTime;
using MongoDB.Driver;
using MongoDB.Driver.Core.Extensions.DiagnosticSources;

namespace ShoppingCart.Api.Infrastructure;

public static class Mongo
{
public static IMongoDatabase ConfigureMongo(IConfiguration configuration)
{
NodaTimeSerializers.Register();
var config = configuration.GetSection("Mongo").Get<MongoSettings>();

var settings = MongoClientSettings.FromConnectionString(config!.ConnectionString);

if (config.User != null && config.Password != null) {
settings.Credential = new MongoCredential(
null,
new MongoInternalIdentity("admin", config.User),
new PasswordEvidence(config.Password)
);
}

settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber());
return new MongoClient(settings).GetDatabase(config.Database);
}

public record MongoSettings
{
public string ConnectionString { get; init; } = null!;
public string Database { get; init; } = null!;
public string? User { get; init; }
public string? Password { get; init; }
}
}
13 changes: 13 additions & 0 deletions src/Retail/ShoppingCart.Api/Queries/Carts/UserCartDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Eventuous.Projections.MongoDB.Tools;

namespace ShoppingCart.Api.Queries.Carts;

public record UserCartDocument : ProjectedDocument
{
public UserCartDocument(string Id) : base(Id)
{
}

public string CustomerId { get; set; } = null!;
public string Status { get; set; } = null!;
}
38 changes: 38 additions & 0 deletions src/Retail/ShoppingCart.Api/Queries/Carts/UserCartProjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Eventuous.Projections.MongoDB;
using Eventuous.Subscriptions.Context;
using MongoDB.Driver;
using ShoppingCart.Carts;

namespace ShoppingCart.Api.Queries.Carts;

[Obsolete("Obsolete per Eventuous; use new API instead (TODO)")]
public class UserCartProjection : MongoProjection<UserCartDocument>
{
public UserCartProjection(IMongoDatabase database) : base(database)
{
On<CartEvents.V1.CartOpened>(stream => stream.GetId(), Handle);

On<CartEvents.V1.CartConfirmed>(builder => builder
.UpdateOne
.DefaultId()
.Update((evt, update) =>
update.Set(x => x.Status, nameof(CartStatus.Confirmed))));

On<CartEvents.V1.CartCancelled>(builder => builder
.UpdateOne
.DefaultId()
.Update((evt, update) =>
update.Set(x => x.Status, nameof(CartStatus.Cancelled))));
}

private static UpdateDefinition<UserCartDocument> Handle(
IMessageConsumeContext<CartEvents.V1.CartOpened> ctx,
UpdateDefinitionBuilder<UserCartDocument> update)
{
var evt = ctx.Message;

return update.SetOnInsert(x => x.Id, ctx.Stream.GetId())
.Set(x => x.CustomerId, evt.CustomerId)
.Set(x => x.Status, nameof(CartStatus.Opened));
}
}
26 changes: 23 additions & 3 deletions src/Retail/ShoppingCart.Api/Registrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
using Eventuous;
using Eventuous.Diagnostics.OpenTelemetry;
using Eventuous.EventStore;
using Eventuous.EventStore.Subscriptions;
using Eventuous.Projections.MongoDB;
using Eventuous.Subscriptions.Registrations;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using ShoppingCart.Api.Infrastructure;
using ShoppingCart.Api.Queries.Carts;
using ShoppingCart.Carts;
using ShoppingCart.Inventories;
using ShoppingCart.Prices;
using ShoppingCart.Products;

#pragma warning disable CS0618 // Type or member is obsolete

Expand All @@ -18,7 +24,6 @@ namespace ShoppingCart.Api;
public static class Registrations
{
private const string OTelServiceName = "shoppingcart";
private const string PostgresSchemaName = "shoppingcart";

public static void AddEventuous(this IServiceCollection services, IConfiguration configuration)
{
Expand All @@ -37,8 +42,23 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration

// other internal and core services
services.AddSingleton<ICombIdGenerator, CombIdGenerator>();
services.AddSingleton<ICheckInventoryService, CheckInventoryService>();
services.AddSingleton<IQuotePriceService, QuotePriceService>();
services.AddSingleton<IProductValidator, ProductValidator>();
services.AddSingleton<IInventoryChecker, InventoryChecker>();
services.AddSingleton<IPriceQuoter, PriceQuoter>();

// subscriptions: checkpoint stores
services.AddSingleton(Mongo.ConfigureMongo(configuration));
services.AddCheckpointStore<MongoCheckpointStore>();

// subscriptions: projections
services.AddSubscription<AllStreamSubscription, AllStreamSubscriptionOptions>(
"UserCartProjections",
builder => builder
.UseCheckpointStore<MongoCheckpointStore>()
.AddEventHandler<UserCartProjection>()
.WithPartitioningByStream(2));

// TODO: add additional mongo, postgresql, and other custom projections

// health checks for subscription service
services
Expand Down
3 changes: 3 additions & 0 deletions src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
<PackageReference Include="Eventuous.Application" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.EventStore" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.Extensions.DependencyInjection" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.Projections.MongoDB" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.AspNetCore.Web" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.Spyglass" Version="$(EventuousVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.6" />
<PackageReference Include="MongoDb.Bson.NodaTime" Version="3.0.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.OpenTelemetry" Version="1.0.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.4.0-rc.4" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.9.0" />
Expand Down
11 changes: 11 additions & 0 deletions src/Retail/ShoppingCart.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,16 @@
},
"EventStore": {
"ConnectionString": "esdb://localhost:2113?tls=false"
},
"Mongo": {
"ConnectionString": "mongodb://localhost:27017",
"User": "mongoadmin",
"Password": "secret",
"Database": "ShoppingCart"
},
"Postgres": {
"ConnectionString": "Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=Password123!;Include Error Detail=true;",
"Schema": "shopping_cart",
"InitializeDatabase": true
}
}
4 changes: 2 additions & 2 deletions src/Retail/ShoppingCart/Carts/CartCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public record RemoveProductFromCart(
int Quantity
);

public record ConfirmCartForCheckout(
public record ConfirmCart(
string CartId
);

public record ConfirmCart(
public record CancelCart(
string CartId
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Retail/ShoppingCart/Carts/CartEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public record CartConfirmed(
string CartId
);

[EventType("V1.CartCancelled")]
public record CartCancelled(
string CartId
);

[EventType("V1.EmptyCartDetected")]
public record EmptyCartDetected(
string CartId
Expand Down
19 changes: 15 additions & 4 deletions src/Retail/ShoppingCart/Carts/CartFuncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ public CartFuncService(
OnExisting<Commands.RemoveProductFromCart>(cmd
=> GetStream(cmd.CartId), RemoveProductFromCart);

OnExisting<Commands.ConfirmCartForCheckout>(cmd
=> GetStream(cmd.CartId), ConfirmCartForCheckout);
OnExisting<Commands.ConfirmCart>(cmd
=> GetStream(cmd.CartId), ConfirmCart);

OnExisting<Commands.CancelCart>(cmd
=> GetStream(cmd.CartId), CancelCart);

static StreamName GetStream(string id) => new($"Cart-{id}");

Expand Down Expand Up @@ -66,13 +69,21 @@ static IEnumerable<object> RemoveProductFromCart(
yield return new Events.EmptyCartDetected(cmd.CartId);
}

static IEnumerable<object> ConfirmCartForCheckout(
static IEnumerable<object> ConfirmCart(
CartState state,
object[] originalEvents,
Commands.ConfirmCartForCheckout cmd)
Commands.ConfirmCart cmd)
{
if (state.CanProceedToCheckout())
yield return new Events.CartConfirmed(cmd.CartId);
}

static IEnumerable<object> CancelCart(
CartState state,
object[] originalEvents,
Commands.CancelCart cmd)
{
yield return new Events.CartCancelled(cmd.CartId);
}
}
}
16 changes: 13 additions & 3 deletions src/Retail/ShoppingCart/Carts/CartState.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ecommerce.Eventuous.Exceptions;
using Eventuous;
using ShoppingCart.Products;
using static ShoppingCart.Carts.CartEvents.V1;

namespace ShoppingCart.Carts;
Expand All @@ -19,6 +20,7 @@ public CartState()
On<ProductAddedToCart>(Handle);
On<ProductRemovedFromCart>(Handle);
On<CartConfirmed>(Handle);
On<CartCancelled>(Handle);
}

private static CartState Handle(CartState state, CartOpened @event) => state with
Expand All @@ -37,19 +39,27 @@ private static CartState Handle(CartState state, CartOpened @event) => state wit

private static CartState Handle(CartState state, ProductRemovedFromCart @event) => state.Status switch
{
CartStatus.Confirmed => throw InvalidStateChangeException.For<CartState, ProductAddedToCart>(state.Id, CartStatus.Confirmed),
CartStatus.Confirmed => throw InvalidStateChangeException.For<CartState, ProductRemovedFromCart>(state.Id, CartStatus.Confirmed),
_ => state with { ProductItems = state.ProductItems.Remove(ProductItem.From(@event.ProductId, @event.Quantity)) }
};

private static CartState Handle(CartState state, CartConfirmed @event) => state.Status switch
{
CartStatus.Confirmed => throw InvalidStateChangeException.For<CartState, ProductAddedToCart>(state.Id, CartStatus.Confirmed),
CartStatus.Confirmed => throw InvalidStateChangeException.For<CartState, CartConfirmed>(state.Id, CartStatus.Confirmed),
_ => state with { Status = CartStatus.Confirmed }
};

private static CartState Handle(CartState state, CartCancelled @event) => state.Status switch
{
CartStatus.Confirmed => throw InvalidStateChangeException.For<CartState, CartCancelled>(state.Id, CartStatus.Confirmed),
CartStatus.Cancelled => throw InvalidStateChangeException.For<CartState, CartCancelled>(state.Id, CartStatus.Cancelled),
_ => state with { Status = CartStatus.Cancelled }
};

public bool CanProceedToCheckout() => Status switch
{
CartStatus.Unset or CartStatus.Opened => false,
CartStatus.Unset => false,
CartStatus.Confirmed => false,
_ => HasProductItems
};
}
3 changes: 2 additions & 1 deletion src/Retail/ShoppingCart/Carts/CartStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public enum CartStatus
{
Unset = 0,
Opened = 1,
Confirmed = 2
Confirmed = 2,
Cancelled = 4
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using ShoppingCart.Carts;
using ShoppingCart.Products;

namespace ShoppingCart.Inventories;

public interface ICheckInventoryService
public interface IInventoryChecker
{
IReadOnlyList<ProductItem> Check(params ProductId[] productIds);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using ShoppingCart.Carts;
using ShoppingCart.Products;

namespace ShoppingCart.Inventories;

public class CheckInventoryService : ICheckInventoryService
public class InventoryChecker : IInventoryChecker
{
public IReadOnlyList<ProductItem> Check(params ProductId[] productIds)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using ShoppingCart.Carts;
using ShoppingCart.Products;

namespace ShoppingCart.Prices;

public interface IQuotePriceService
public interface IPriceQuoter
{
IReadOnlyList<PricedProductItem> Quote(params ProductItem[] productItems);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using ShoppingCart.Carts;
using ShoppingCart.Products;

namespace ShoppingCart.Prices;

public class QuotePriceService : IQuotePriceService
public class PriceQuoter : IPriceQuoter
{
public IReadOnlyList<PricedProductItem> Quote(params ProductItem[] productItems)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Retail/ShoppingCart/Products/IProductValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ShoppingCart.Products;

public interface IProductValidator
{
IReadOnlyList<(ProductId productId, bool isValid)> Quote(params ProductId[] productIds);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ShoppingCart.Carts;
namespace ShoppingCart.Products;

public class PricedProductItem
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Eventuous;

namespace ShoppingCart.Carts;
namespace ShoppingCart.Products;

public record ProductId(string Value) : Id(Value);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ShoppingCart.Carts;
namespace ShoppingCart.Products;

public record ProductItem
{
Expand Down
Loading

0 comments on commit 8d73b66

Please sign in to comment.