Skip to content

Commit

Permalink
Retail Cart Initial Use Cases (2024-06-23) (#48)
Browse files Browse the repository at this point in the history
* expanded functionality of carts module

* rename, cleanup, you know

* items in cart reminder event

* renamed and expanded http for carts

* initial cart projection, cleaned up confirm process, cart services
  • Loading branch information
erikshafer authored Jun 24, 2024
1 parent e922a48 commit 3bb694c
Show file tree
Hide file tree
Showing 33 changed files with 584 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ProductQueryApi : ControllerBase

[HttpGet]
[Route("{id}")]
public async Task<ProductState> GetProduct(string id, CancellationToken ct)
public async Task<ProductState> Get(string id, CancellationToken ct)
{
var product = await _store.Load<Product>(StreamName.For<Product>(id), ct);
return product.State;
Expand Down
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
2 changes: 1 addition & 1 deletion src/Inventory/Inventory.Api/HttpApi/InventoryQueryApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class InventoryQueryApi : ControllerBase

[HttpGet]
[Route("{id}")]
public async Task<InventoryState> GetInventory(string id, CancellationToken ct)
public async Task<InventoryState> Get(string id, CancellationToken ct)
{
// TODO: Is there a way to query the AggregateStory without a proper Aggregate, and just State?
var product = await _store.Load<Inventories.Inventory>(StreamName.For<Inventories.Inventory>(id), ct);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,47 @@
using Eventuous;
using Eventuous.AspNetCore.Web;
using Microsoft.AspNetCore.Mvc;
using static ShoppingCart.CartCommands.V1;
using ShoppingCart.Carts;

namespace ShoppingCart.Api.HttpApi;
namespace ShoppingCart.Api.HttpApi.Carts;

[Route("/cart")]
public class CommandApi : CommandHttpApiBaseFunc<CartState>
public class CartCommandApi : CommandHttpApiBaseFunc<CartState>
{
private readonly IFuncCommandService<CartState> _service;

public CommandApi(IFuncCommandService<CartState> service) : base(service)
public CartCommandApi(IFuncCommandService<CartState> service) : base(service)
{
_service = service;
}

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

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

[HttpPost]
[Route("remove-product")]
public async Task<ActionResult<Result>> OpenCart([FromBody] RemoveProductFromCart cmd, CancellationToken ct)
{
var result = await _service.Handle(cmd, ct);
return Ok(result);
}

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

[HttpPost]
[Route("confirm")]
public async Task<ActionResult<Result>> OpenCart([FromBody] ConfirmCart cmd, CancellationToken ct)
public async Task<ActionResult<Result>> OpenCart([FromBody] CartCommands.V1.ConfirmCart cmd, CancellationToken ct)
{
var result = await _service.Handle(cmd, ct);
return Ok(result);
Expand Down
22 changes: 22 additions & 0 deletions src/Retail/ShoppingCart.Api/HttpApi/Carts/CartQueryApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Eventuous;
using Microsoft.AspNetCore.Mvc;
using ShoppingCart.Carts;

namespace ShoppingCart.Api.HttpApi.Carts;

[Route("/carts")]
public class CartQueryApi : ControllerBase
{
private readonly IAggregateStore _store;

public CartQueryApi(IAggregateStore store) => _store = store;

[HttpGet]
[Route("{id}")]
public async Task<CartState> Get(string id, CancellationToken ct)
{
// TODO: Is there a way to query the AggregateStory without a proper Aggregate, and just State?
var product = await _store.Load<Cart>(StreamName.For<Cart>(id), ct);
return product.State;
}
}
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));
}
}
27 changes: 26 additions & 1 deletion src/Retail/ShoppingCart.Api/Registrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +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 @@ -15,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 @@ -34,6 +42,23 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration

// other internal and core services
services.AddSingleton<ICombIdGenerator, CombIdGenerator>();
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
108 changes: 108 additions & 0 deletions src/Retail/ShoppingCart.Api/ShoppingCart.Api.Carts.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
@ShoppingCart.Api_HostAddress = http://localhost:5262
@cartId = 0190483c-e260-4c97-aa62-0d90d41ba833
@productId = 36606-001
@customerId = erik-123

###

# curl -X 'GET'
# 'http://localhost:5262/carts/{{cartId}}'
# -H 'accept: text/plain'
GET http://localhost:5262/carts/{{cartId}}
accept: text/plain

###

# curl -X 'POST'
# 'http://localhost:5262/cart/open'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "customerId": "{{customerId}}"
#}'
POST http://localhost:5262/cart/open
accept: text/plain
Content-Type: application/json

{
"customerId": "{{customerId}}"
}

###

# curl -X 'POST'
# 'http://localhost:5262/cart/add-product'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "cartId": "{{cartId}}",
# "productId": "{{productId}}",
# "quantity": 2
#}'
POST http://localhost:5262/cart/add-product
accept: text/plain
Content-Type: application/json

{
"cartId": "{{cartId}}",
"productId": "{{productId}}",
"quantity": 2
}

###

# curl -X 'POST'
# 'http://localhost:5262/cart/remove-product'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "cartId": "{{shoppingCartId}",
# "productId": "{{productId}}",
# "quantity": 1
#}'
POST http://localhost:5262/cart/remove-product
accept: text/plain
Content-Type: application/json

{
"cartId": "{{cartId}}",
"productId": "{{productId}}",
"quantity": 1
}

###

# curl -X 'POST'
# 'http://localhost:5262/cart/prepare-checkout'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "cartId": "{{cartId}}"
#}'
POST http://localhost:5262/cart/prepare-checkout
accept: text/plain
Content-Type: application/json

{
"cartId": "{{cartId}}"
}

###

# curl -X 'POST'
# 'http://localhost:5262/cart/confirm'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "cartId": "{{cartId}"
#}'
POST http://localhost:5262/cart/confirm
accept: text/plain
Content-Type: application/json

{
"cartId": "{{cartId}}"
}

###

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
6 changes: 0 additions & 6 deletions src/Retail/ShoppingCart.Api/ShoppingCart.Api.http

This file was deleted.

Loading

0 comments on commit 3bb694c

Please sign in to comment.