Skip to content

Commit

Permalink
Migrate and Extend Price Module (#41)
Browse files Browse the repository at this point in the history
* removed pricing folder and related dotnet projects

* re-introduced Products directory in Catalog module

* Swagger schema changes, initial price use cases impl'd, HTTP file

* initial barebones state projection of Price, added HTTP calls
  • Loading branch information
erikshafer authored Jun 19, 2024
1 parent 04fac49 commit 6e39adb
Show file tree
Hide file tree
Showing 62 changed files with 649 additions and 599 deletions.
30 changes: 0 additions & 30 deletions EventSourcingEcommerce.sln
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inventory", "src\Inventory\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inventory.Api", "src\Inventory\Inventory.Api\Inventory.Api.csproj", "{75428565-8144-4AED-B8C8-19F7DDCDAAC6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pricing", "Pricing", "{E8FE7001-AFB6-4FB2-B922-BB6A203A652E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prices", "src\Pricing\Prices\Prices.csproj", "{62D164F6-13C2-4B93-B01E-95D09B60C59B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prices.Api", "src\Pricing\Prices.Api\Prices.Api.csproj", "{B96B27DD-C084-4310-9056-1E084795427C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discounts", "src\Pricing\Discounts\Discounts.csproj", "{090F1820-19D9-464B-A0B0-415182075D9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discounts.Api", "src\Pricing\Discounts.Api\Discounts.Api.csproj", "{F42EAACF-D74A-4646-A311-AB9EB131AE8A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Retail", "Retail", "{91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shopping-cart", "shopping-cart", "{E8D6C05C-F653-42AA-BB9B-6E57251C7E99}"
Expand Down Expand Up @@ -125,22 +115,6 @@ Global
{75428565-8144-4AED-B8C8-19F7DDCDAAC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75428565-8144-4AED-B8C8-19F7DDCDAAC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75428565-8144-4AED-B8C8-19F7DDCDAAC6}.Release|Any CPU.Build.0 = Release|Any CPU
{62D164F6-13C2-4B93-B01E-95D09B60C59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62D164F6-13C2-4B93-B01E-95D09B60C59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D164F6-13C2-4B93-B01E-95D09B60C59B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D164F6-13C2-4B93-B01E-95D09B60C59B}.Release|Any CPU.Build.0 = Release|Any CPU
{B96B27DD-C084-4310-9056-1E084795427C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B96B27DD-C084-4310-9056-1E084795427C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B96B27DD-C084-4310-9056-1E084795427C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B96B27DD-C084-4310-9056-1E084795427C}.Release|Any CPU.Build.0 = Release|Any CPU
{090F1820-19D9-464B-A0B0-415182075D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{090F1820-19D9-464B-A0B0-415182075D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{090F1820-19D9-464B-A0B0-415182075D9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{090F1820-19D9-464B-A0B0-415182075D9D}.Release|Any CPU.Build.0 = Release|Any CPU
{F42EAACF-D74A-4646-A311-AB9EB131AE8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F42EAACF-D74A-4646-A311-AB9EB131AE8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F42EAACF-D74A-4646-A311-AB9EB131AE8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F42EAACF-D74A-4646-A311-AB9EB131AE8A}.Release|Any CPU.Build.0 = Release|Any CPU
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -168,10 +142,6 @@ Global
{53645A48-B20D-4DD5-9AF3-3478C3443E14} = {920605B8-81CA-4D01-B404-6A83E995FE6D}
{CECCECD0-A286-481A-AC43-8D911BFB2E91} = {1DB2150C-70C5-47D5-B4FC-5C0190106E75}
{75428565-8144-4AED-B8C8-19F7DDCDAAC6} = {1DB2150C-70C5-47D5-B4FC-5C0190106E75}
{62D164F6-13C2-4B93-B01E-95D09B60C59B} = {E8FE7001-AFB6-4FB2-B922-BB6A203A652E}
{B96B27DD-C084-4310-9056-1E084795427C} = {E8FE7001-AFB6-4FB2-B922-BB6A203A652E}
{090F1820-19D9-464B-A0B0-415182075D9D} = {E8FE7001-AFB6-4FB2-B922-BB6A203A652E}
{F42EAACF-D74A-4646-A311-AB9EB131AE8A} = {E8FE7001-AFB6-4FB2-B922-BB6A203A652E}
{E8D6C05C-F653-42AA-BB9B-6E57251C7E99} = {91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}
{3C54979E-C2BB-448E-86EE-08F366B973DF} = {5C748417-563A-41A4-AC92-67A815C5A929}
{4280FEC2-9CB2-48B2-B565-17E81AF40333} = {91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}
Expand Down
80 changes: 80 additions & 0 deletions src/Catalog/Catalog.Api/Catalog.Api.Prices.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@Catalog.Api_HostAddress = http://localhost:5252

###

GET {{Catalog.Api_HostAddress}}/api/
Accept: application/json

###

# curl -X 'POST'
# 'http://localhost:5252/price/initialize'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "sku": "36606-001",
# "minimumAdvertisedPrice": 94.00,
# "manufacturerSuggestedRetailPrice": 99.99,
# "bundledQuantity": 0,
# "bundledPrice": 0,
# "currency": "USD",
# "createdAt": "2024-06-19T13:25:47.714Z",
# "createdBy": "Erik"
#}'
POST http://localhost:5252/price/initialize
accept: text/plain
Content-Type: application/json

{
"sku": "36606-001",
"minimumAdvertisedPrice": 94.00,
"manufacturerSuggestedRetailPrice": 99.99,
"bundledQuantity": 0,
"bundledPrice": 0,
"currency": "USD",
"createdAt": "2024-06-19T13:25:47.714Z",
"createdBy": "Erik"
}

###

# curl -X 'POST'
# 'http://localhost:5252/price/activate'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "priceId": "1252983042692087808",
# "activatedBy": "Erik"
#}'
POST http://localhost:5252/price/activate
accept: text/plain
Content-Type: application/json

{
"priceId": "1252983042692087808",
"activatedBy": "Erik"
}

###

# curl -X 'POST'
# 'http://localhost:5252/price/deprecate'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "priceId": "1252983042692087808",
# "deprecatedBy": "Erik",
# "reason": "goof"
#}'
POST http://localhost:5252/price/deprecate
accept: text/plain
Content-Type: application/json

{
"priceId": "1252983042692087808",
"deprecatedBy": "Erik",
"reason": "goof"
}

###

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@Inventory.Api_HostAddress = http://localhost:5252
@Catalog.Api_HostAddress = http://localhost:5252

###

GET {{Inventory.Api_HostAddress}}/api/
GET {{Catalog.Api_HostAddress}}/api/
Accept: application/json

###
Expand Down
43 changes: 43 additions & 0 deletions src/Catalog/Catalog.Api/Commands/PriceCommandService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Catalog.Prices;
using Ecommerce.Core.Identities;
using Eventuous;

namespace Catalog.Api.Commands;

public class PriceCommandService : CommandService<Price, PriceState, PriceId>
{
[Obsolete("Obsolete usage of OnNewAsync per Eventuous; use new API instead (TODO)")]
public PriceCommandService(
IAggregateStore store,
Services.IsSkuAvailable isSkuAvailable,
Services.IsUserAuthorized isUserAuthorized,
ISnowflakeIdGenerator idGenerator)
: base(store)
{
var generatedId = idGenerator.New();
OnNewAsync<PriceCommands.Initialize>(cmd => new PriceId(generatedId),
((price, cmd, _) => price.Draft(
generatedId,
cmd.Sku,
cmd.MinimumAdvertisedPrice,
cmd.ManufacturerSuggestedRetailPrice,
cmd.BundledQuantity,
cmd.BundledPrice,
cmd.Currency,
DateTimeOffset.Now,
cmd.CreatedBy,
isSkuAvailable,
isUserAuthorized)));

OnExisting<PriceCommands.Activate>(cmd => new PriceId(cmd.PriceId),
((price, cmd) => price.Activate(
DateTimeOffset.Now,
cmd.ActivatedBy)));

OnExisting<PriceCommands.Deprecate>(cmd => new PriceId(cmd.PriceId),
((price, cmd) => price.Deprecate(
DateTimeOffset.Now,
cmd.DeprecatedBy,
cmd.Reason)));
}
}
26 changes: 26 additions & 0 deletions src/Catalog/Catalog.Api/Commands/PriceCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Catalog.Api.Commands;

public static class PriceCommands
{
public record Initialize(
string Sku,
decimal MinimumAdvertisedPrice,
decimal ManufacturerSuggestedRetailPrice,
int BundledQuantity,
decimal BundledPrice,
string Currency,
DateTimeOffset CreatedAt,
string CreatedBy
);

public record Activate(
string PriceId,
string ActivatedBy
);

public record Deprecate(
string PriceId,
string DeprecatedBy,
string Reason
);
}
1 change: 1 addition & 0 deletions src/Catalog/Catalog.Api/Commands/ProductCommandService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Catalog.Products;
using Ecommerce.Core.Identities;
using Eventuous;
using static Catalog.Api.Commands.ProductCommands;
Expand Down
26 changes: 26 additions & 0 deletions src/Catalog/Catalog.Api/HttpApi/PriceCommandApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Catalog.Prices;
using Eventuous;
using Eventuous.AspNetCore.Web;
using Microsoft.AspNetCore.Mvc;
using static Catalog.Api.Commands.PriceCommands;

namespace Catalog.Api.HttpApi;

[Route("/price")]
public class PriceCommandApi(ICommandService<Price> service) : CommandHttpApiBase<Price>(service)
{
[HttpPost]
[Route("initialize")]
public Task<ActionResult<Result>> Draft([FromBody] Initialize cmd, CancellationToken ct)
=> Handle(cmd, ct);

[HttpPost]
[Route("activate")]
public Task<ActionResult<Result>> Activate([FromBody] Activate cmd, CancellationToken ct)
=> Handle(cmd, ct);

[HttpPost]
[Route("deprecate")]
public Task<ActionResult<Result>> Deprecate([FromBody] Deprecate cmd, CancellationToken ct)
=> Handle(cmd, ct);
}
21 changes: 21 additions & 0 deletions src/Catalog/Catalog.Api/HttpApi/PriceQueryApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Catalog.Prices;
using Eventuous;
using Microsoft.AspNetCore.Mvc;

namespace Catalog.Api.HttpApi;

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

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

[HttpGet]
[Route("{id}")]
public async Task<PriceState> GetPrice(string id, CancellationToken ct)
{
var product = await _store.Load<Price>(StreamName.For<Price>(id), ct);
return product.State;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Catalog.Products;
using Eventuous;
using Eventuous.AspNetCore.Web;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -6,7 +7,7 @@
namespace Catalog.Api.HttpApi;

[Route("/product")]
public class CommandApi(ICommandService<Product> service) : CommandHttpApiBase<Product>(service)
public class ProductCommandApi(ICommandService<Product> service) : CommandHttpApiBase<Product>(service)
{
[HttpPost]
[Route("draft-with-id")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
using Catalog.Products;
using Eventuous;
using Microsoft.AspNetCore.Mvc;

namespace Catalog.Api.HttpApi;

[Route("/products")]
public class QueryApi : ControllerBase
public class ProductQueryApi : ControllerBase
{
private readonly IAggregateStore _store;

public QueryApi(IAggregateStore store) => _store = store;
public ProductQueryApi(IAggregateStore store) => _store = store;

[HttpGet]
[Route("{id}")]
public async Task<ProductState> GetProduct(string id, CancellationToken ct)
{
// Eventuous will throw a AggregateNotFoundException if the stream does not exist
var product = await _store.Load<Product>(StreamName.For<Product>(id), ct);
return product.State;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Catalog/Catalog.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
options.CustomSchemaIds(type => type.ToString());
options.CustomSchemaIds(type => type.FullName?.Replace("+", "."));
});
builder.Services.AddTelemetry();
builder.Services.AddEventuous(builder.Configuration);
builder.Services.AddEventuousSpyglass();
Expand Down
18 changes: 18 additions & 0 deletions src/Catalog/Catalog.Api/Queries/PriceDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Eventuous.Projections.MongoDB.Tools;

namespace Catalog.Api.Queries;

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

public string Sku { get; set; } = null!;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.MinValue;
public string CreatedBy { get; set; } = null!;
public string Status { get; set; } = null!;
public decimal MinimumAdvertisedPrice { get; set; }
public decimal ManufacturerSuggestedRetailPrice { get; set; }
// TODO: Bundled Pricing
}
50 changes: 50 additions & 0 deletions src/Catalog/Catalog.Api/Queries/PriceStateProjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Catalog.Prices;
using Eventuous.Projections.MongoDB;
using Eventuous.Subscriptions.Context;
using MongoDB.Driver;
using static Catalog.Prices.PriceEvents;

namespace Catalog.Api.Queries;

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

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

return update.SetOnInsert(x => x.Id, ctx.Stream.GetId())
.Set(x => x.Sku, evt.Sku)
.Set(x => x.CreatedAt, evt.CreatedAt)
.Set(x => x.CreatedBy, evt.CreatedBy)
.Set(x => x.Status, nameof(PriceStatus.Initialized))
.Set(x => x.MinimumAdvertisedPrice, evt.MinimumAdvertisedPrice)
.Set(x => x.ManufacturerSuggestedRetailPrice, evt.ManufacturerSuggestedRetailPrice);
// TODO: bundled pricing, etc
}

private static UpdateDefinition<PriceDocument> Handle(
IMessageConsumeContext<V1.PriceActivated> ctx,
UpdateDefinition<PriceDocument> update)
{
var @event = ctx.Message;

return update.Set(x => x.Status, nameof(PriceStatus.Activated));
}

private static UpdateDefinition<PriceDocument> Handle(
IMessageConsumeContext<V1.PriceDeprecated> ctx,
UpdateDefinition<PriceDocument> update)
{
var @event = ctx.Message;

return update.Set(x => x.Status, nameof(PriceStatus.Deprecated));
}
}
3 changes: 2 additions & 1 deletion src/Catalog/Catalog.Api/Queries/ProductStateProjection.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Catalog.Products;
using Eventuous.Projections.MongoDB;
using Eventuous.Subscriptions.Context;
using MongoDB.Driver;
using static Catalog.ProductEvents;
using static Catalog.Products.ProductEvents;

namespace Catalog.Api.Queries;

Expand Down
Loading

0 comments on commit 6e39adb

Please sign in to comment.