Skip to content

Commit

Permalink
Improved the Cashier domain. TODO: update UI
Browse files Browse the repository at this point in the history
Added CreateOrder, AddItemToOrder, PlaceOrder commands and its related events/handlers.
  • Loading branch information
fredimachado committed May 23, 2024
1 parent 298dbee commit 3710d0c
Show file tree
Hide file tree
Showing 27 changed files with 498 additions and 206 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageVersion Include="AspNetCore.HealthChecks.Rabbitmq" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.5.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
Expand Down
24 changes: 19 additions & 5 deletions src/Cashier/NCafe.Cashier.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using NCafe.Cashier.Domain.Queries;
using NCafe.Cashier.Api.Projections;
using NCafe.Cashier.Domain.Commands;
using NCafe.Cashier.Domain.Queries;
using NCafe.Cashier.Domain.ReadModels;
using NCafe.Infrastructure;
using NCafe.Cashier.Api.Projections;
using NCafe.Core.Queries;
using NCafe.Core.Commands;
using NCafe.Core.Queries;
using NCafe.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

Expand Down Expand Up @@ -59,11 +59,25 @@
})
.WithName("GetProducts");

app.MapPost("/orders", async (ICommandDispatcher commandDispatcher, PlaceOrder command) =>
app.MapPost("/orders", async (ICommandDispatcher commandDispatcher, CreateOrder command) =>
{
await commandDispatcher.DispatchAsync(command);
return Results.Created("/orders", null);
})
.WithName("CreateOrder");

app.MapPost("/orders/{id:guid}/item", async (ICommandDispatcher commandDispatcher, Guid id, AddItemToOrder command) =>
{
await commandDispatcher.DispatchAsync(command);
return Results.Accepted();
})
.WithName("AddItemToOrder");

app.MapPost("/orders/{id:guid}/place", async (ICommandDispatcher commandDispatcher, Guid id, PlaceOrder command) =>
{
await commandDispatcher.DispatchAsync(command);
return Results.Accepted();
})
.WithName("PlaceOrder");

app.MapGet("/healthz", () => "OK");
Expand Down
112 changes: 112 additions & 0 deletions src/Cashier/NCafe.Cashier.Domain.Tests/Commands/AddItemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using NCafe.Cashier.Domain.Commands;
using NCafe.Cashier.Domain.Entities;
using NCafe.Cashier.Domain.Exceptions;
using NCafe.Cashier.Domain.ReadModels;
using NCafe.Cashier.Domain.ValueObjects;
using NCafe.Core.ReadModels;
using NCafe.Core.Repositories;

namespace NCafe.Cashier.Domain.Tests.Commands;

public class AddItemTests
{
private readonly AddItemToOrderHandler _sut;
private readonly IRepository _repository;
private readonly IReadModelRepository<Product> _productRepository;

public AddItemTests()
{
_repository = A.Fake<IRepository>();
_productRepository = A.Fake<IReadModelRepository<Product>>();
_sut = new AddItemToOrderHandler(_repository, _productRepository);
}

[Fact]
public async Task ShouldSaveOrder()
{
// Arrange
var orderId = Guid.NewGuid();
var productId = Guid.NewGuid();
var quantity = 1;
var command = new AddItemToOrder(orderId, productId, quantity);

A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(new Order(orderId, "cashier-1", DateTimeOffset.UtcNow)));

A.CallTo(() => _productRepository.GetById(command.ProductId))
.Returns(new Product { Name = "Latte", Price = 5 });

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeNull();
A.CallTo(() => _repository.Save(A<Order>.That.Matches(o => o.Id == orderId &&
o.Items.Any(i => i.ProductId == productId && i.Quantity == quantity))))
.MustHaveHappenedOnceExactly();
}

[Fact]
public async Task GivenOrderNotFound_ShouldThrow()
{
// Arrange
var orderId = Guid.NewGuid();
var productId = Guid.NewGuid();
var quantity = 1;
var command = new AddItemToOrder(orderId, productId, quantity);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeOfType<OrderNotFoundException>();
}

[Fact]
public async Task GivenProductNotFound_ShouldThrow()
{
// Arrange
var orderId = Guid.NewGuid();
var productId = Guid.NewGuid();
var quantity = 1;
var command = new AddItemToOrder(orderId, productId, quantity);

A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(new Order(orderId, "cashier-1", DateTimeOffset.UtcNow)));

A.CallTo(() => _productRepository.GetById(command.ProductId))
.Returns(null);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeOfType<ProductNotFoundException>();
}

[Fact]
public async Task GivenNotNewOrder_ShouldThrow()
{
// Arrange
var orderId = Guid.NewGuid();
var order = new Order(orderId, "cashier-1", DateTimeOffset.UtcNow);
var productId = Guid.NewGuid();
var quantity = 1;
var command = new AddItemToOrder(orderId, productId, quantity);

order.AddItem(new OrderItem(Guid.NewGuid(), 1, "Latte", 5));
order.PlaceOrder(new Customer("John Doe"), DateTimeOffset.UtcNow);

A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(order));

A.CallTo(() => _productRepository.GetById(command.ProductId))
.Returns(new Product { Name = "Latte", Price = 5 });

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeOfType<CannotAddItemToOrderException>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.Extensions.Time.Testing;
using NCafe.Cashier.Domain.Commands;
using NCafe.Cashier.Domain.Entities;
using NCafe.Core.Repositories;

namespace NCafe.Cashier.Domain.Tests.Commands;

public class CreateOrderTests
{
private readonly CreateOrderHandler _sut;
private readonly IRepository _repository;
private readonly FakeTimeProvider _timeProvider;

public CreateOrderTests()
{
_repository = A.Fake<IRepository>();
_timeProvider = new FakeTimeProvider();
_sut = new CreateOrderHandler(_repository, _timeProvider);
}

[Fact]
public async Task ShouldSaveOrder()
{
// Arrange
var createdAt = DateTimeOffset.UtcNow;
_timeProvider.SetUtcNow(createdAt);
var createdBy = "cashier-1";
var command = new CreateOrder(createdBy);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeNull();
A.CallTo(() => _repository.Save(A<Order>.That.Matches(o => o.Status == OrderStatus.New &&
o.CreatedBy == createdBy &&
o.CreatedAt == createdAt)))
.MustHaveHappenedOnceExactly();
}
}

This file was deleted.

71 changes: 42 additions & 29 deletions src/Cashier/NCafe.Cashier.Domain.Tests/Commands/PlaceOrderTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using NCafe.Cashier.Domain.Commands;
using Microsoft.Extensions.Time.Testing;
using NCafe.Cashier.Domain.Commands;
using NCafe.Cashier.Domain.Entities;
using NCafe.Cashier.Domain.Exceptions;
using NCafe.Cashier.Domain.Messages;
using NCafe.Cashier.Domain.ReadModels;
using NCafe.Cashier.Domain.ValueObjects;
using NCafe.Core.MessageBus;
using NCafe.Core.ReadModels;
using NCafe.Core.Repositories;

namespace NCafe.Cashier.Domain.Tests.Commands;
Expand All @@ -13,69 +12,83 @@ public class PlaceOrderTests
{
private readonly PlaceOrderHandler _sut;
private readonly IRepository _repository;
private readonly IReadModelRepository<Product> _productRepository;
private readonly IPublisher _publisher;
private readonly FakeTimeProvider _timeProvider;

public PlaceOrderTests()
{
_repository = A.Fake<IRepository>();
_productRepository = A.Fake<IReadModelRepository<Product>>();
_publisher = A.Fake<IPublisher>();
_sut = new PlaceOrderHandler(_repository, _productRepository, _publisher);
_timeProvider = new FakeTimeProvider();
_sut = new PlaceOrderHandler(_repository, _publisher, _timeProvider);
}

[Fact]
public async Task GivenProductNotFound_ShouldThrowException()
public async Task PlaceOrder_ShouldSaveOrder()
{
// Arrange
var productId = Guid.NewGuid();
A.CallTo(() => _productRepository.GetById(productId))
.Returns(null);
var orderId = Guid.NewGuid();
var order = new Order(orderId, "cashier-1", DateTimeOffset.UtcNow);
var placedAt = DateTimeOffset.UtcNow;
var customer = new Customer("John Doe");
var command = new PlaceOrder(orderId, customer);

var command = new PlaceOrder(productId, 1);
_timeProvider.SetUtcNow(placedAt);

A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(order));

order.AddItem(new OrderItem(Guid.NewGuid(), 1, "Latte", 5));

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeOfType<ProductNotFoundException>();
exception.ShouldBeNull();
A.CallTo(() => _repository.Save(A<Order>.That.Matches(o => o.Status == OrderStatus.Placed &&
o.Customer == customer &&
o.PlacedAt == placedAt)))
.MustHaveHappenedOnceExactly();
}

[Fact]
public async Task GivenProductExists_ShouldSaveOrder()
public async Task GivenNotNewOrder_ShouldThrow()
{
// Arrange
var productId = Guid.NewGuid();
A.CallTo(() => _productRepository.GetById(productId))
.Returns(new Product { Id = productId });
var orderId = Guid.NewGuid();
var order = new Order(orderId, "cashier-1", DateTimeOffset.UtcNow);
var customer = new Customer("John Doe");
var command = new PlaceOrder(orderId, customer);

order.AddItem(new OrderItem(Guid.NewGuid(), 1, "Latte", 5));
order.PlaceOrder(new Customer("John Doe"), DateTimeOffset.UtcNow);

var command = new PlaceOrder(productId, 1);
A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(order));

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeNull();
A.CallTo(() => _repository.Save(A<Order>.That.Matches(o => o.ProductId == productId && o.Quantity == 1)))
.MustHaveHappenedOnceExactly();
exception.ShouldBeOfType<CannotPlaceOrderException>();
}

[Fact]
public async Task GivenOrderSaved_ShouldPublishToMessageBus()
public async Task GivenNoItemAdded_ShouldThrow()
{
// Arrange
var productId = Guid.NewGuid();
A.CallTo(() => _productRepository.GetById(productId))
.Returns(new Product { Id = productId });
var orderId = Guid.NewGuid();
var order = new Order(orderId, "cashier-1", DateTimeOffset.UtcNow);
var customer = new Customer("John Doe");
var command = new PlaceOrder(orderId, customer);

var command = new PlaceOrder(productId, 1);
A.CallTo(() => _repository.GetById<Order>(orderId))
.Returns(Task.FromResult(order));

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));

// Assert
exception.ShouldBeNull();
A.CallTo(() => _publisher.Publish(A<OrderPlaced>.That.Matches(o => o.ProductId == productId && o.Quantity == 1)))
.MustHaveHappenedOnceExactly();
exception.ShouldBeOfType<CannotPlaceEmptyOrderException>();
}
}
Loading

0 comments on commit 3710d0c

Please sign in to comment.