Design Document
Overview
Este design define a arquitetura e componentes para implementar o endpoint DELETE api/cart/{cartId}/items/{cartItemId} no coezzion-service-cart. O endpoint permite que o checkout service remova um CartItem originado de uma recomendação aceita anteriormente, aplicando soft-delete no item, desvinculando a recomendação e recalculando os totais do cart.
O fluxo segue o padrão existente da Cart API: Controller → CartService.Handle(command) → Repository, usando BaseResult para respostas de negócio. O handler valida o cart, o item e a origem, delega a lógica de domínio ao CartModel.RevertRecommendedItem(), e persiste.
Contexto
- Endpoint chamado exclusivamente pelo coezzion-service-checkout via
x-api-key: "internal" - Consumidor:
CartIntegrationServiceno checkout service - Semântica idempotente: cart/item não encontrado retorna 200 silencioso
Decisões de Design
| # | Decisão | Justificativa |
|---|---|---|
| 1 | Handler via CartService.Handle(RemoveCartItemCommand) — não ZZMediator | Padrão existente: ICartService com sobrecargas de Handle |
| 2 | Idempotente para cart/item não encontrado (200 silencioso) | DELETE é idempotente; consistente com padrão do add-item |
| 3 | EF Core com tracking via GetByIdWithRecommendationsAsync | Necessário para persist via change tracking; método já existe |
| 4 | Sem body no command — ambos params da rota | DELETE sem body; controller cria command manualmente |
| 5 | ApiKey "internal" com [AllowAnonymous] + [UseApiKey("internal")] + [AddSchema] | Mesmo padrão do POST api/cart/{cartId}/items |
| 6 | Response 200 sem corpo | Consistente com DeleteCartCommand; checkout relê via GetCartInfoAsync |
| 7 | Lógica de domínio delegada a CartModel.RevertRecommendedItem(cartItemId) | Encapsula soft-delete + desvincular recomendação + recalcular total |
| 8 | Erros de persistência retornam 400 (não 500) | Padrão existente: CustomResponse(BaseResult) produz 400 para todos os erros |
Architecture
Diagrama de Fluxo
Camadas e Responsabilidades
| Camada | Componente | Tipo | Responsabilidade |
|---|---|---|---|
| API | CartController | Modificado | Novo endpoint DELETE api/cart/{cartId}/items/{cartItemId} |
| Domain | RemoveCartItemCommand | Novo | DTO do command com CartId e CartItemId |
| API | CartService | Modificado | Handler: validações + orquestração + persistência |
| Domain | ICartService | Modificado | Nova assinatura Handle(RemoveCartItemCommand) |
| DB-Core | CartModel.RevertRecommendedItem | Já existe | Lógica de domínio: soft-delete + desvincular + recalcular |
Components and Interfaces
Novos Arquivos
| Arquivo | Camada | Tipo |
|---|---|---|
src/Cart.Domain/Commands/RemoveCartItemCommand.cs | Domain | Command DTO |
Arquivos Modificados
| Arquivo | Modificação |
|---|---|
src/Cart.API/Controllers/CartController.cs | Novo endpoint DELETE |
src/Cart.Domain/Services/ICartService.cs | Nova assinatura Handle(RemoveCartItemCommand) |
src/Cart.API/Application/CartService.cs | Implementação do handler |
Arquivos de Teste
| Arquivo | Tipo |
|---|---|
src/Cart.UnitTests/Application/Commands/RemoveCartItemHandlerTests.cs | Teste unitário do handler |
Interface — ICartService (nova sobrecarga)
public interface ICartService
{
// ... handles existentes ...
Task<BaseResult> Handle(RemoveCartItemCommand command); // NOVO
}
Controller — Novo Endpoint
/// <summary>
/// Remove um item recomendado do carrinho
/// </summary>
/// <response code="200">Item removido com sucesso ou idempotente (cart/item não encontrado)</response>
/// <response code="400">Falha na validação</response>
/// <response code="401">ApiKey inválida</response>
[HttpDelete("{cartId:int}/items/{cartItemId:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[AllowAnonymous]
[UseApiKey("internal")]
[AddSchema]
public async Task<IActionResult> RemoveCartItem(int cartId, int cartItemId)
{
var command = new RemoveCartItemCommand { CartId = cartId, CartItemId = cartItemId };
var result = await _cartService.Handle(command);
return CustomResponse(result);
}
Handler — CartService.Handle(RemoveCartItemCommand)
public async Task<BaseResult> Handle(RemoveCartItemCommand command)
{
var baseResult = new BaseResult();
var cart = await _cartRepository.GetByIdWithRecommendationsAsync(command.CartId);
if (cart is null)
return baseResult;
if (cart.SaleEcommerce)
{
baseResult.Errors.Add("Operacao nao disponivel para este tipo de pedido");
return baseResult;
}
var item = cart.CartItems.FirstOrDefault(i => i.Id == command.CartItemId);
if (item is null)
return baseResult;
if (!item.FromRecommendation)
{
baseResult.Errors.Add("Item nao originado de recomendacao");
return baseResult;
}
cart.RevertRecommendedItem(command.CartItemId);
try
{
_cartRepository.Update(cart);
await _cartRepository.SaveAsync();
}
catch (Exception)
{
baseResult.Errors.Add("Erro interno ao processar a requisicao");
return baseResult;
}
return baseResult;
}
Nota sobre
CustomResponse(BaseResult): Quandoresult.IsValid()é true eresult.Responseé null,CustomResponseretornaOk(null)→ HTTP 200 sem corpo. Quando há erros, retornaBadRequestcom os erros → HTTP 400.
Data Models
RemoveCartItemCommand (Novo)
namespace Cart.Domain.Commands;
public class RemoveCartItemCommand
{
public int CartId { get; set; }
public int CartItemId { get; set; }
}
Sem
[JsonIgnore]: Diferente doAddItemToCartCommandondeCartIdtem[JsonIgnore](vem da rota mas body contémRecommendedItemId), oRemoveCartItemCommandnão possui body. Ambas propriedades setadas pelo controller a partir de route params.
Entidades Existentes (NÃO modificar — coezzion-db-core)
// CartModel — método de domínio (Core.OrgDB.Entities.CartModel.cs:209-229)
public class CartModel
{
public int Id { get; set; }
public bool SaleEcommerce { get; set; }
public ICollection<CartItemsModel> CartItems { get; set; }
public ICollection<CartItemRecommendationModel> CartItemRecommendations { get; set; }
public void RevertRecommendedItem(int cartItemId)
{
var item = CartItems.FirstOrDefault(p => p.Id == cartItemId);
if (item != null)
{
if (item.FromRecommendation)
{
item.Delete();
var recommendation = CartItemRecommendations.FirstOrDefault(p => p.CartItemId == cartItemId);
if (recommendation != null)
{
recommendation.CartItem = null;
recommendation.CartItemId = null;
}
CalculateTotal();
}
}
}
public void CalculateTotal()
{
ItemsTotal = CartItems.Where(p => !p.DateDeleted.HasValue).Sum(p => p.CalculateTotal());
}
}
// CartItemsModel (Core.OrgDB.Entities.CartItemsModel.cs:57)
public class CartItemsModel
{
public int Id { get; set; }
public bool FromRecommendation { get; set; } // default: false
}
// CartItemRecommendationModel
public class CartItemRecommendationModel
{
public int Id { get; set; }
public int? CartItemId { get; set; } // null = disponível para re-adição
public CartItemsModel CartItem { get; set; }
}
Diagrama de Relacionamento
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Handler idempotency for missing resources
For any cartId that does not correspond to an existing cart, or for any cartItemId that does not exist in the cart's CartItems collection, the handler SHALL return a valid BaseResult without errors (HTTP 200), and SHALL NOT call Update or SaveAsync on the repository.
Validates: Requirements 3.2, 5.2
Property 2: Validation guards reject invalid state
For any cart where SaleEcommerce is true, or for any item where FromRecommendation is false, the handler SHALL return a BaseResult containing exactly one error message and SHALL NOT call Update or SaveAsync on the repository.
Validates: Requirements 4.1, 6.1
Error Handling
| Cenário | HTTP Status | Mensagem | Responsável |
|---|---|---|---|
| ApiKey ausente/inválida | 401 | (sem body) | UseApiKeyAttribute (filtro) |
cartId/cartItemId inválidos na rota | 400 | (ASP.NET route binding) | Framework |
| Cart não encontrado | 200 | (sem erro — idempotente) | Handler retorna BaseResult válido |
SaleEcommerce == true | 400 | "Operacao nao disponivel para este tipo de pedido" | Handler |
| Item não encontrado | 200 | (sem erro — idempotente) | Handler retorna BaseResult válido |
FromRecommendation == false | 400 | "Item nao originado de recomendacao" | Handler |
| Sucesso — item removido | 200 | (sem corpo) | Handler retorna BaseResult válido |
| Erro de persistência | 400 | "Erro interno ao processar a requisicao" | Handler (catch) |
400 vs 500 para erros de persistência: O padrão existente do codebase usa
CustomResponse(BaseResult)que produz 400 para todos os erros. Os handlersAddItemToCartCommandeChangeCartDataCommandusam o mesmo padrãobaseResult.Errors.Add(...)no catch → 400. Este design mantém consistência com o codebase.