Skip to main content

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: CartIntegrationService no checkout service
  • Semântica idempotente: cart/item não encontrado retorna 200 silencioso

Decisões de Design​

#DecisãoJustificativa
1Handler via CartService.Handle(RemoveCartItemCommand) — não ZZMediatorPadrão existente: ICartService com sobrecargas de Handle
2Idempotente para cart/item não encontrado (200 silencioso)DELETE é idempotente; consistente com padrão do add-item
3EF Core com tracking via GetByIdWithRecommendationsAsyncNecessário para persist via change tracking; método já existe
4Sem body no command — ambos params da rotaDELETE sem body; controller cria command manualmente
5ApiKey "internal" com [AllowAnonymous] + [UseApiKey("internal")] + [AddSchema]Mesmo padrão do POST api/cart/{cartId}/items
6Response 200 sem corpoConsistente com DeleteCartCommand; checkout relê via GetCartInfoAsync
7Lógica de domínio delegada a CartModel.RevertRecommendedItem(cartItemId)Encapsula soft-delete + desvincular recomendação + recalcular total
8Erros 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​

CamadaComponenteTipoResponsabilidade
APICartControllerModificadoNovo endpoint DELETE api/cart/{cartId}/items/{cartItemId}
DomainRemoveCartItemCommandNovoDTO do command com CartId e CartItemId
APICartServiceModificadoHandler: validações + orquestração + persistência
DomainICartServiceModificadoNova assinatura Handle(RemoveCartItemCommand)
DB-CoreCartModel.RevertRecommendedItemJá existeLógica de domínio: soft-delete + desvincular + recalcular

Components and Interfaces​

Novos Arquivos​

ArquivoCamadaTipo
src/Cart.Domain/Commands/RemoveCartItemCommand.csDomainCommand DTO

Arquivos Modificados​

ArquivoModificação
src/Cart.API/Controllers/CartController.csNovo endpoint DELETE
src/Cart.Domain/Services/ICartService.csNova assinatura Handle(RemoveCartItemCommand)
src/Cart.API/Application/CartService.csImplementação do handler

Arquivos de Teste​

ArquivoTipo
src/Cart.UnitTests/Application/Commands/RemoveCartItemHandlerTests.csTeste 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): Quando result.IsValid() é true e result.Response é null, CustomResponse retorna Ok(null) → HTTP 200 sem corpo. Quando há erros, retorna BadRequest com 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 do AddItemToCartCommand onde CartId tem [JsonIgnore] (vem da rota mas body contém RecommendedItemId), o RemoveCartItemCommand nã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árioHTTP StatusMensagemResponsável
ApiKey ausente/inválida401(sem body)UseApiKeyAttribute (filtro)
cartId/cartItemId inválidos na rota400(ASP.NET route binding)Framework
Cart não encontrado200(sem erro — idempotente)Handler retorna BaseResult válido
SaleEcommerce == true400"Operacao nao disponivel para este tipo de pedido"Handler
Item não encontrado200(sem erro — idempotente)Handler retorna BaseResult válido
FromRecommendation == false400"Item nao originado de recomendacao"Handler
Sucesso — item removido200(sem corpo)Handler retorna BaseResult válido
Erro de persistência400"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 handlers AddItemToCartCommand e ChangeCartDataCommand usam o mesmo padrão baseResult.Errors.Add(...) no catch → 400. Este design mantém consistência com o codebase.

Testing Strategy​

Abordagem​

Este feature utiliza testes unitários example-based com xUnit + Moq como estratégia principal. As propriedades de correctness (idempotência e rejeição de guards) são verificáveis via testes example-based dado que os cenários são finitos e discretos — não há transformação de dados com amplitude de inputs que justifique 100+ iterações randomizadas.

Razões para não usar PBT com generators randomizados:

  • O handler é uma operação CRUD com guards sequenciais booleanos
  • Cada branch é determinístico (null/not-null, true/false)
  • A lógica de domínio (RevertRecommendedItem) já existe no db-core
  • 7 cenários example-based cobrem 100% dos branches do handler

Cenários de Teste​

#CenárioMock SetupResultado Esperado
1Cart não encontradoGetByIdWithRecommendationsAsync → nullBaseResult válido, sem erros, Update não chamado
2SaleEcommerce == trueCart com SaleEcommerce = trueBaseResult com erro "Operacao nao disponivel para este tipo de pedido"
3Item não encontrado na coleçãocartItemId inexistente em CartItemsBaseResult válido, sem erros, Update não chamado
4FromRecommendation == falseItem encontrado com FromRecommendation = falseBaseResult com erro "Item nao originado de recomendacao"
5Sucesso — item removido e persistidoDados válidosBaseResult válido; Update once; SaveAsync once
6Sucesso — recomendação desvinculadaDados válidos com recommendation vinculadarecommendation.CartItemId == null; item com DateDeleted setado
7Erro de persistênciaSaveAsync lança ExceptionBaseResult com erro "Erro interno ao processar a requisicao"

Estrutura do Teste​

  • Arquivo: src/Cart.UnitTests/Application/Commands/RemoveCartItemHandlerTests.cs
  • Classe: RemoveCartItemHandlerTests
  • Trait: [Trait("Layer", "Application - Commands")]
  • Instanciação: CartService com todos os mocks (mesmo padrão dos testes existentes)
  • Nomenclatura: Handle_[Cenário]_[ResultadoEsperado]

Dependências de Teste​

MockUsado nos cenários
ICartRepository.GetByIdWithRecommendationsAsyncTodos (1-7)
ICartRepository.Update5, 6, 7
ICartRepository.SaveAsync5, 6, 7

Dependências​

DependênciaStatusNota
CartModel.RevertRecommendedItem(int)🟢 disponívelCartModel.cs:209-229
CartModel.CalculateTotal() — filtra DateDeleted🟢 disponívelCartModel.cs:233
CartItemsModel.FromRecommendation (bool)🟢 disponívelCartItemsModel.cs:57
ICartRepository.GetByIdWithRecommendationsAsync(int)🟢 disponívelUsado pelo AddItemToCartCommand
CartRepository.Update() — invalida cache🟢 disponívelLinha 79
UseApiKeyAttribute🟢 disponívelCart.API.Filters
AddSchemaAttribute🟢 disponívelCart.API.Configuration
BaseController.CustomResponse(BaseResult)🟢 disponívelCoezzion.Common.Controllers
BaseResult🟢 disponívelCoezzion.Common.Communication