Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Minimal API and Service layer for eCommerce #155

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts.Api/Carts.Api.csproj
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Carter" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Carts\Carts.csproj" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts.Api/Program.cs
@@ -0,0 +1,9 @@
using Carter;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarter();

var app = builder.Build();
app.MapCarter();

app.Run();
53 changes: 53 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartModule.cs
@@ -0,0 +1,53 @@
using Carter;
using Carter.Request;
using Carts.ShoppingCarts;
using Carts.ShoppingCarts.AddingProduct;
using Carts.ShoppingCarts.CancellingCart;
using Carts.ShoppingCarts.ConfirmingCart;
using Carts.ShoppingCarts.OpeningCart;
using Carts.ShoppingCarts.RemovingProduct;

public class ShoppingCartModule: ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapPost("/shoppingcart", OpenCart);
app.MapPost("/shoppingcart/{cartId:guid}/products", AddProduct);
app.MapDelete("/shoppingcart/{cartId:guid}/products/{productId:guid}", RemoveProduct);
app.MapPut("/shoppingcart/{cartId:guid}/confirmation", ConfirmCart);
app.MapDelete("/shoppingcart/{cartId:guid}", CancelCart);
}

private IResult CancelCart(HttpContext context, Guid cartId, ICancelCartService cancelCartService)
{
cancelCartService.CancelCart(cartId);
return Results.StatusCode(204);
}

private IResult ConfirmCart(HttpContext context, Guid cartId, IConfirmCartService confirmCartService)
{
confirmCartService.Confirm(cartId);
return Results.StatusCode(204);
}

private IResult RemoveProduct(HttpContext context, Guid cartId, Guid productId,
IRemoveProductService removeProductService)
{
removeProductService.RemoveProduct(cartId, productId, context.Request.Query.As<int?>("quantity"),
context.Request.Query.As<decimal?>("unitPrice"));
return Results.StatusCode(204);
}

private IResult AddProduct(HttpContext context, Guid cartId, AddProductRequest model,
IAddProductService addProductService)
{
addProductService.AddProduct(cartId, model.ProductId, model.Quantity);
return Results.StatusCode(204);
}

private IResult OpenCart(HttpContext context, OpenShoppingCartRequest model, IOpenCartService openCartService)
{
var cartId = openCartService.OpenCart(model.ClientId);
return Results.Created($"/shoppingcart/{cartId}", null);
}
}
@@ -0,0 +1,27 @@
namespace Carts.ShoppingCarts;

public record OpenShoppingCartRequest(
Guid ClientId
);

public record AddProductRequest(
Guid ProductId,
int Quantity
);

public record PricedProductItemRequest(
Guid? ProductId,
int? Quantity,
decimal? UnitPrice
);

public record RemoveProductRequest(
PricedProductItemRequest? ProductItem
);

public record ConfirmShoppingCartRequest;

public record GetCartAtVersionRequest(
Guid? CartId,
long? Version
);
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.json
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
13 changes: 13 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts/Carts.csproj
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\Core\Core.csproj" />
</ItemGroup>

</Project>
@@ -0,0 +1,7 @@
namespace Carts.DataAccess;

public interface IShoppingCartRepository
{
ShoppingCart GetById(Guid id);
void Save(ShoppingCart cart);
}
@@ -0,0 +1,8 @@
using Carts.ShoppingCarts.Products;

namespace Carts.Pricing;

public interface IProductPriceCalculator
{
IReadOnlyList<PricedProductItem> Calculate(params ProductItem[] productItems);
}
@@ -0,0 +1,19 @@
using Carts.ShoppingCarts.Products;

namespace Carts.Pricing;

public class RandomProductPriceCalculator: IProductPriceCalculator
{
public IReadOnlyList<PricedProductItem> Calculate(params ProductItem[] productItems)
{
if (productItems.Length == 0)
throw new ArgumentOutOfRangeException(nameof(productItems.Length));

var random = new Random();

return productItems
.Select(pi =>
PricedProductItem.Create(pi, Math.Round(new decimal(random.NextDouble() * 100), 2)))
.ToList();
}
}
114 changes: 114 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCart.cs
@@ -0,0 +1,114 @@
using Carts.Pricing;
using Carts.ShoppingCarts.Products;
using Core.Extensions;

namespace Carts;

public class ShoppingCart
{
public Guid Id { get; private set; }

public Guid ClientId { get; private set; }

public ShoppingCartStatus Status { get; private set; }

public IList<PricedProductItem> ProductItems { get; private set; } = default!;

public Money TotalPrice => ProductItems.Sum(pi => pi.TotalPrice);

public static ShoppingCart Open(
Guid cartId,
Guid clientId)
{
return new ShoppingCart(cartId, clientId);
}

public ShoppingCart() { }

private ShoppingCart(
Guid id,
Guid clientId)
{
Id = id;
ClientId = clientId;
ProductItems = new List<PricedProductItem>();
Status = ShoppingCartStatus.Pending;
}

public void AddProduct(
IProductPriceCalculator productPriceCalculator,
ProductItem productItem)
{
if (Status != ShoppingCartStatus.Pending)
throw new InvalidOperationException($"Adding product for the cart in '{Status}' status is not allowed.");

var pricedProductItem = productPriceCalculator.Calculate(productItem).Single();

var existingProductItem = FindProductItemMatchingWith(pricedProductItem);

if (existingProductItem is null)
{
ProductItems.Add(pricedProductItem);
return;
}

ProductItems.Replace(
existingProductItem,
existingProductItem.MergeWith(pricedProductItem)
);
}


public void RemoveProduct(
PricedProductItem productItemToBeRemoved)
{
if (Status != ShoppingCartStatus.Pending)
throw new InvalidOperationException($"Removing product from the cart in '{Status}' status is not allowed.");

var existingProductItem = FindProductItemMatchingWith(productItemToBeRemoved);

if (existingProductItem is null)
throw new InvalidOperationException(
$"Product with id `{productItemToBeRemoved.ProductId}` and price '{productItemToBeRemoved.UnitPrice}' was not found in cart.");

if (!existingProductItem.HasEnough(productItemToBeRemoved.Quantity))
throw new InvalidOperationException(
$"Cannot remove {productItemToBeRemoved.Quantity} items of Product with id `{productItemToBeRemoved.ProductId}` as there are only ${existingProductItem.Quantity} items in card");


if (existingProductItem.HasTheSameQuantity(productItemToBeRemoved))
{
ProductItems.Remove(existingProductItem);
return;
}

ProductItems.Replace(
existingProductItem,
existingProductItem.Subtract(productItemToBeRemoved)
);
}

public void Confirm()
{
if (Status != ShoppingCartStatus.Pending)
throw new InvalidOperationException($"Confirming cart in '{Status}' status is not allowed.");

Status = ShoppingCartStatus.Confirmed;
}


public void Cancel()
{
if (Status != ShoppingCartStatus.Pending)
throw new InvalidOperationException($"Canceling cart in '{Status}' status is not allowed.");


Status = ShoppingCartStatus.Canceled;
}

private PricedProductItem? FindProductItemMatchingWith(PricedProductItem productItem)
{
return ProductItems
.SingleOrDefault(pi => pi.MatchesProductAndPrice(productItem));
}
}
8 changes: 8 additions & 0 deletions Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCartStatus.cs
@@ -0,0 +1,8 @@
namespace Carts;

public enum ShoppingCartStatus
{
Pending = 1,
Confirmed = 2,
Canceled = 4
}
@@ -0,0 +1,25 @@
using Carts.DataAccess;
using Carts.Pricing;
using Carts.ShoppingCarts.Products;

namespace Carts.ShoppingCarts.AddingProduct;

public class AddProductService: IAddProductService
{
private readonly IProductPriceCalculator productPriceCalculator;
private readonly IShoppingCartRepository shoppingCartRepository;

public AddProductService(IProductPriceCalculator productPriceCalculator,
IShoppingCartRepository shoppingCartRepository)
{
this.productPriceCalculator = productPriceCalculator;
this.shoppingCartRepository = shoppingCartRepository;
}

public void AddProduct(Guid cartId, Guid productId, int quantity)
{
var cart = this.shoppingCartRepository.GetById(cartId);
cart.AddProduct(this.productPriceCalculator, ProductItem.Create(productId, quantity));
this.shoppingCartRepository.Save(cart);
}
}
@@ -0,0 +1,6 @@
namespace Carts.ShoppingCarts.AddingProduct;

public interface IAddProductService
{
void AddProduct(Guid cartId, Guid productId, int quantity);
}
@@ -0,0 +1,20 @@
using Carts.DataAccess;

namespace Carts.ShoppingCarts.CancellingCart;

public class CancelCartService: ICancelCartService
{
private readonly IShoppingCartRepository shoppingCartRepository;

public CancelCartService(IShoppingCartRepository shoppingCartRepository)
{
this.shoppingCartRepository = shoppingCartRepository;
}

public void CancelCart(Guid cartId)
{
var cart = this.shoppingCartRepository.GetById(cartId);
cart.Cancel();
this.shoppingCartRepository.Save(cart);
}
}
@@ -0,0 +1,6 @@
namespace Carts.ShoppingCarts.CancellingCart;

public interface ICancelCartService
{
void CancelCart(Guid cartId);
}
@@ -0,0 +1,20 @@
using Carts.DataAccess;

namespace Carts.ShoppingCarts.ConfirmingCart;

public class ConfirmCartService: IConfirmCartService
{
private readonly IShoppingCartRepository shoppingCartRepository;

public ConfirmCartService(IShoppingCartRepository shoppingCartRepository)
{
this.shoppingCartRepository = shoppingCartRepository;
}

public void Confirm(Guid cartId)
{
var cart = this.shoppingCartRepository.GetById(cartId);
cart.Confirm();
this.shoppingCartRepository.Save(cart);
}
}