Skip to content

Commit

Permalink
Allow user to delete a product (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
aforesti authored Jun 2, 2024
1 parent f2d2917 commit 3fe614e
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 15 deletions.
6 changes: 6 additions & 0 deletions src/Admin/NCafe.Admin.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
})
.WithName("CreateProduct");

app.MapDelete("/products/{id}", async (IMediator mediator, Guid id) =>
{
await mediator.Send(new DeleteProduct(id));
return Results.NoContent();
}).WithName("DeleteProduct");

app.MapGet("/healthz", () => "OK");

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public ProductProjectionService(IProjectionService<Product> projectionService)
Name = @event.Name,
Price = @event.Price
});

projectionService.OnUpdate<ProductDeleted>(
product => product.Id,
(@event, product) => product.IsDeleted = true);


}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down
49 changes: 49 additions & 0 deletions src/Admin/NCafe.Admin.Domain.Tests/Commands/DeleteProductTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using NCafe.Admin.Domain.Commands;
using NCafe.Admin.Domain.Entities;
using NCafe.Admin.Domain.Exceptions;
using NCafe.Core.Repositories;
using System.Threading;

namespace NCafe.Admin.Domain.Tests.Commands;

public class DeleteProductTests
{
private readonly DeleteProductHandler _sut;
private readonly IRepository _repository;

public DeleteProductTests()
{
_repository = A.Fake<IRepository>();
_sut = new DeleteProductHandler(_repository);
}

[Fact]
public async Task GivenNonExistingProduct_ShouldThrowException()
{
// Arrange
var id = Guid.NewGuid();
A.CallTo(() => _repository.GetById<Product>(id)).Returns((Product)null);

// Act
var exception = await Record.ExceptionAsync(() => _sut.Handle(new DeleteProduct(id), CancellationToken.None));

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

[Fact]
public async Task GivenExistingProduct_ShouldDeleteProduct()
{
// Arrange
var id = Guid.NewGuid();
var product = new Product(id, "Latte", 3);
A.CallTo(() => _repository.GetById<Product>(id)).Returns(product);

// Act
await _sut.Handle(new DeleteProduct(id), CancellationToken.None);

// Assert
A.CallTo(() => _repository.Save(A<Product>.That.Matches(p => p.Id == id && p.IsDeleted == true)))
.MustHaveHappenedOnceExactly();
}
}
25 changes: 25 additions & 0 deletions src/Admin/NCafe.Admin.Domain/Commands/DeleteProduct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using MediatR;
using NCafe.Admin.Domain.Entities;
using NCafe.Admin.Domain.Exceptions;
using NCafe.Core.Repositories;

namespace NCafe.Admin.Domain.Commands;

public record DeleteProduct(Guid Id) : IRequest;

internal sealed class DeleteProductHandler(IRepository repository) : IRequestHandler<DeleteProduct>
{
private readonly IRepository _repository = repository;

public async Task Handle(DeleteProduct command, CancellationToken cancellationToken)
{
var product = await _repository.GetById<Product>(command.Id);
if (product is null || product.IsDeleted)
{
throw new ProductNotFound(command.Id);
}

product.Delete();
await _repository.Save(product);
}
}
11 changes: 11 additions & 0 deletions src/Admin/NCafe.Admin.Domain/Entities/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,22 @@ public Product(Guid id, string name, decimal price)

public string Name { get; private set; }
public decimal Price { get; private set; }
public bool IsDeleted { get; private set; }

private void Apply(ProductCreated @event)
{
Id = @event.Id;
Name = @event.Name;
Price = @event.Price;
}

public void Delete()
{
RaiseEvent(new ProductDeleted(Id));
}

private void Apply(ProductDeleted _)
{
IsDeleted = true;
}
}
11 changes: 11 additions & 0 deletions src/Admin/NCafe.Admin.Domain/Events/ProductDeleted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using NCafe.Core.Domain;

namespace NCafe.Admin.Domain.Events;

public sealed record ProductDeleted : Event
{
public ProductDeleted(Guid id)
{
Id = id;
}
}
5 changes: 5 additions & 0 deletions src/Admin/NCafe.Admin.Domain/Exceptions/ProductNotFound.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using NCafe.Core.Exceptions;

namespace NCafe.Admin.Domain.Exceptions;

public class ProductNotFound(Guid id) : DomainException($"Product with id '{id}' was not found.");
1 change: 1 addition & 0 deletions src/Admin/NCafe.Admin.Domain/Queries/GetProducts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class GetProductsHandler(IReadModelRepository<Product> productRe
public Task<Product[]> Handle(GetProducts query, CancellationToken cancellation)
{
var products = _productRepository.GetAll()
.Where(p => !p.IsDeleted)
.ToArray();
return Task.FromResult(products);
}
Expand Down
1 change: 1 addition & 0 deletions src/Admin/NCafe.Admin.Domain/ReadModels/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public sealed class Product : ReadModel
{
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; }
}
2 changes: 1 addition & 1 deletion src/Cashier/NCafe.Cashier.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@

app.Run();

public partial class Program { }
public partial class Program;
11 changes: 11 additions & 0 deletions src/Cashier/NCafe.Cashier.Api/Projections/ProductDeleted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using NCafe.Core.Domain;

namespace NCafe.Cashier.Api.Projections;

public sealed record ProductDeleted : Event
{
public ProductDeleted(Guid id)
{
Id = id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ public ProductProjectionService(IProjectionService<Product> projectionService)
{
Id = @event.Id,
Name = @event.Name,
Price = @event.Price
Price = @event.Price,
});

projectionService.OnUpdate<ProductDeleted>(
product => product.Id,
(@event, product) => product.IsDeleted = true);
}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down
1 change: 1 addition & 0 deletions src/Cashier/NCafe.Cashier.Domain/Queries/GetProducts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal sealed class GetProductsHandler(IReadModelRepository<Product> productRe
public Task<Product[]> Handle(GetProducts request, CancellationToken cancellationToken)
{
var products = _productRepository.GetAll()
.Where(p => !p.IsDeleted)
.ToArray();
return Task.FromResult(products);
}
Expand Down
1 change: 1 addition & 0 deletions src/Cashier/NCafe.Cashier.Domain/ReadModels/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public sealed class Product : ReadModel
{
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using EventStore.Client;
using EventStore.Client;
using Microsoft.Extensions.Logging;
using NCafe.Core.Domain;
using NCafe.Core.Projections;
Expand Down Expand Up @@ -51,7 +51,8 @@ private Task EventAppeared(StreamSubscription subscription, ResolvedEvent @event

private void SubscriptionDropped(StreamSubscription subscription, SubscriptionDroppedReason reason, Exception exception)
{
_logger.LogError("Subscription Dropped.");
_logger.LogError("Subscription Dropped for '{EventStoreStream}' with reason '{SubscriptionDroppedReason}'", subscription.SubscriptionId, reason);
_logger.LogError(exception, "{Exception}", exception.Message);
}

public void OnCreate<TEvent>(Func<TEvent, T> handler) where TEvent : Event
Expand Down
60 changes: 49 additions & 11 deletions src/UI/NCafe.Web/Pages/Admin/Index.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@page "/admin"
@page "/admin"
@using NCafe.Web.Models
@inject HttpClient Http
@inject IConfiguration Configuration
@inject NavigationManager NavigationManager
@inject ModalService _modalService
@inject IMessageService _message

<PageTitle>Admin - NCafe</PageTitle>

Expand All @@ -22,37 +24,73 @@
to help mitigate this in a later moment.
</p>

@if (products == null)
@if (_products == null)
{
<Spin />
return;
}
else if (!products.Any())

@if (!_products.Any())
{
<Result Title="No products to show." />
return;
}
else
{


<Table TItem="Product"
DataSource="@products"
Total="@(products.Length)"
DataSource="@_products"
Total="@(_products.Length)"
HidePagination="true">
<Column @bind-Field="@context.Id" />
<Column @bind-Field="@context.Name" Sortable />
<Column @bind-Field="@context.Price" Format="C2" Sortable />
<ActionColumn Title="Action">
<Space>
<SpaceItem><Button Danger OnClick="@(() => ShowDeleteConfirm(context.Id))">Delete</Button></SpaceItem>
</Space>
</ActionColumn>
</Table>
}


@code {
private Product[] products;
private Product[] _products;

protected override async Task OnInitializedAsync()
{
var url = $"{Configuration["AdminBaseAddress"]}/products";
products = await Http.GetFromJsonAsync<Product[]>(url);
_products = await Http.GetFromJsonAsync<Product[]>(url);
}

void CreateProduct()
{
NavigationManager.NavigateTo("admin/create-product");
}


private async Task ShowDeleteConfirm(Guid id)
{
var productName = _products.Single(p => p.Id == id).Name;
var confirmed = await _modalService.ConfirmAsync(new ConfirmOptions
{
Title = "Delete item",
Content = $"Are you sure you want to delete {productName}?",
OkType = "danger",
Icon = @<Icon Type="exclamation-circle" Theme="outline"></Icon>
});

if (confirmed)
{
await DeleteItem(id);
}
}

private async Task DeleteItem(Guid id)
{
var url = $"{Configuration["AdminBaseAddress"]}/products/{id}";
var response = await Http.DeleteAsync(url);
response.EnsureSuccessStatusCode();

var product = _products.Single(p => p.Id == id);
await _message.Info($"{product.Name} has been deleted.");
_products = _products.Where(p => p.Id != id).ToArray();
}
}

0 comments on commit 3fe614e

Please sign in to comment.