Requisitos — Remove Recommended Item
Task #194882 · Endpoint DELETE api/payment/v2/{id:guid}/items/{cartItemId:int}
Task pai: #194882 — [Back] 04 - Remover item recomendado Contexto: US 194871 — MVP Upsell (Pedido) Spec tecnica: a ser criada ADRs relacionados: ADR-005 · ADR-007 · ADR-008 · ADR-009
Status: refinado Sessão de grilling: 2026-06-19
Visão geral
O cliente que adicionou um item recomendado ao pedido via zzlink pode desistir e removê-lo antes de finalizar o pagamento. O endpoint recebe o identificador do item já presente no cart, valida que é um item originado de uma recomendação, remove-o via Cart API e retorna os valores atualizados do pedido para que o front possa atualizar a tela sem recarregar.
O handler segue o mesmo padrão do AddRecommendedItemCommandHandler: lock distribuído, validação de status do payment, validação de SaleEcommerce, chamada à Cart API, atualização síncrona do Payment.Total e montagem do ResponseDTO.
Papéis
- Cliente: pessoa que acessa o zzlink para efetuar o pagamento
- Checkout service: orquestrador da operação — valida, chama Cart API, atualiza Payment, registra alllogs
- Cart API: responsável por aplicar soft-delete no item, desvincular a recomendação e recalcular totais do cart (defesa em profundidade)
Requisitos
RF-01 — Receber e validar o contexto do pedido
User Story: Como cliente no zzlink, quero remover um item recomendado que adicionei ao meu pedido, para que ele não seja incluído no pagamento que vou realizar.
Acceptance Criteria:
WHENo cliente chamaDELETE api/payment/v2/{id:guid}/items/{cartItemId:int}THENo sistemaSHALLexigir autenticação via[Authorize(AuthenticationSchemes = "PaymentsScheme")]e validação de schema via[AddSchema](headerapi-company-targetobrigatório). OcartItemIdé informado na rota (route param), não no body — o ASP.NET model binding garante que é umintválido via constraint{cartItemId:int}.WHENo{id}(GUID do payment) não corresponde a nenhum paymentTHENo sistemaSHALLretornar400com mensagem"Payment não encontrado".WHENo payment encontrado temCartId <= 0THENo sistemaSHALLretornar400com mensagem"Pagamento não possui carrinho associado".WHENo payment encontrado temStatusdiferente deCreatedouCreateTransactionFailTHENo sistemaSHALLregistrar na alllogs com prefixo[Invalido]("[Invalido] Tentativa de remover item recomendado {cartItemId} do carrinho {cartId} - {status}") e retornar400com mensagem"Não foi possível remover o item recomendado do pedido.".WHENo payment encontrado temStatusigual aCreatedouCreateTransactionFailTHENo sistemaSHALLprosseguir com o processamento.WHENo cart vinculado ao payment temSaleEcommerce == trueTHENo sistemaSHALLretornar400com mensagem"Operação não disponível para este tipo de pedido".
Ordem das validações no handler: (1) payment não encontrado → (2)
CartId <= 0→ (3)Statusinválido (RF-01.4) → (4) busca cart e validaSaleEcommerce→ (5) item emcart.Items(RF-02) → (6)FromRecommendation == true(RF-02). A validação de Status ocorre antes da busca do cart para evitar query desnecessária quando o payment já está em estado inválido.
Defesa em profundidade: A validação de
SaleEcommerceocorre em duas camadas: (1) no checkout service, que busca oCartInfoDTOviaGetCartInfoAsynce validacart.SaleEcommerce; (2) na Cart API, que validacart.SaleEcommercenoRemoveCartItemHandler. A duplicação é intencional para proteger contra chamadas diretas à Cart API.
RF-02 — Validar o item a remover
Acceptance Criteria:
WHENocartItemIdinformado não corresponde a nenhumCartItemna coleçãocart.Items(viaCartItemsInfoDTO)THENo sistemaSHALLretornar400com mensagem"Item não encontrado no carrinho".WHENoCartItemencontrado temFromRecommendation == falseTHENo sistemaSHALLretornar400com mensagem"Item não removível".
RF-03 — Chamar a Cart API para remover o item
Acceptance Criteria:
WHENtodas as validações anteriores passamTHENo sistemaSHALLchamarDELETE api/cart/{cartId}/items/{cartItemId}viaCartIntegrationService.RemoveRecommendedItemAsync(cartId, cartItemId).WHENa Cart API retorna sucesso (HTTP 200)THENo sistemaSHALLprosseguir para a atualização doPayment.Total.WHENoCartIntegrationServiceretorna erros (falha de rede, timeout, exceção, ou erros de negócio propagados da Cart API)THENo sistemaSHALLregistrar a falha na alllogs com detalhes e retornar os erros propagados viaReturnErrors(cartResult.Errors).
Mensagem genérica de erro: O
CartIntegrationServiceconverte todas as exceções (timeout, conexão, erro HTTP) emBaseResult.Errorscom a mensagem"Não foi possível remover o item". Erros de negócio da Cart API (ex:"Operacao nao disponivel para este tipo de pedido","Item nao originado de recomendacao") são propagados diretamente.
Idempotência fora de escopo: O comportamento idempotente da Cart API (HTTP 200 quando cart/item não existem) é definido e tratado pela Task 194883 — Cart API: Remove Item. As validações prévias deste handler (RF-01, RF-02) garantem que cart e item existem antes da chamada; qualquer divergência pós-chamada é tratada como erro de negócio propagado.
RF-04 — Atualizar o Payment e retornar o DTO consolidado
Acceptance Criteria:
WHENa Cart API confirma sucessoTHENo sistemaSHALLbuscar o cart atualizado viaGetCartInfoAsync(cartId)para obter o novo total.WHENoPayment.Totalé diferente doupdatedCart.TotalTHENo sistemaSHALLatualizar sincronamente o campoTotaldoPaymentModelcom o novo total e persistir viaUpdate+SaveAsync.WHENa atualização doPayment.Totalfalha com exceçãoTHENo sistemaSHALLregistrar a falha na alllogs (mensagem truncada em 512 chars) e retornar400com mensagem"Erro interno ao atualizar o pagamento.".WHENoPayment.Totalé atualizado com sucessoTHENo sistemaSHALLretornar200com oRemoveRecommendedItemResponseDTOcontendo:values—Valuesrecalculado com o novo total (construtornew Values(updatedCart))products—List<Product>completa e atualizada dosCartItems(sem o item removido, cada item comcartItemId)paymentMethods—PaymentMethodscom parcelas recalculadas para o novo totalrecommendations—RecommendationsInfoDTO?(wrapper comHoursToExpireeItemsfiltrados porCartItemId == null; recomendações anteriormente vinculadas ao item removido voltam a ficar disponíveis após o desvinculamento)
Após a remoção, o
CartItemRecommendationModeltemCartItemIdsetado paranullpela Cart API (RevertRecommendedItem), o que faz a recomendação reaparecer na lista de disponíveis (BuildAvailableRecommendationsfiltra porCartItemId == null).
RF-05 — Auditoria (alllogs)
Acceptance Criteria:
WHENo handler inicia o processamento após validação de lockTHENo sistemaSHALLregistrar na alllogs"Tentativa de remover item recomendado {cartItemId} do carrinho {cartId}"com método"Handle-RemoveRecommendedItem".WHENa Cart API retorna errosTHENo sistemaSHALLregistrar na alllogs"Erro ao chamar Cart API para remover item recomendado: {erros_concatenados}"com método"Handle-RemoveRecommendedItem".WHENoPayment.Totalmuda após a remoçãoTHENo sistemaSHALLregistrar na alllogs"Valor do pedido mudou: {last} -> {new}"com método"Handle-RemoveRecommendedItem".WHENocorre exceção ao persistir o paymentTHENo sistemaSHALLregistrar na alllogs"Erro ao persistir pagamento após remover item recomendado: {ex.Message_truncado_512}"com método"Handle-RemoveRecommendedItem".
Nota: O log de status inválido (
[Invalido]) está definido em RF-01.4 (incorporado à validação de contexto).
RF-06 — Lock distribuído
Acceptance Criteria:
WHENo handler inicia o processamentoTHENo sistemaSHALLadquirir lock distribuído viaILockService.LockAsynccom chave"rec-item:{PaymentGuid}"e TTL de 10 segundos.WHENo lock não pode ser adquirido (concorrência)THENo sistemaSHALLretornar HTTP429com mensagem"Pagamento em processamento.".WHENo handler termina (sucesso ou erro)THENo sistemaSHALLliberar o lock viaILockService.ReleaseLockAsync.
Lock key compartilhada com ADD: A chave
"rec-item:{PaymentGuid}"é a mesma usada peloAddRecommendedItemCommandHandler, garantindo exclusão mútua entre operações de adicionar e remover no mesmo payment (evita race condition ADD/REMOVE concorrentes).
Contrato do endpoint
Request
DELETE api/payment/v2/{id:guid}/items/{cartItemId:int}
Authorization: Bearer {token PaymentsScheme}
api-company-target: {schema}
Sem body — RESTful: DELETE de sub-recurso identificado pela rota. Semanticamente correto para DELETE. O ASP.NET model binding valida que
{cartItemId}éintvia route constraint; se não-numérico, a request é rejeitada antes de chegar ao handler.
Response 200
{
"values": {
"products": { "value": 0.0, "description": "R$ 0,00", "amount": 0 },
"discount": { "value": 0.0, "description": "R$ 0,00" },
"discountPix": { "value": 0.0, "description": "R$ 0,00" },
"bonus": { "value": 0.0, "description": "R$ 0,00" },
"subtotal": { "value": 0.0, "description": "R$ 0,00" },
"subtotalWhenPix": { "value": 0.0, "description": "R$ 0,00" },
"shipment": { "value": 0.0, "description": "R$ 0,00" },
"total": { "value": 0.0, "description": "R$ 0,00" },
"totalWhenPix": { "value": 0.0, "description": "R$ 0,00" }
},
"products": [
{
"name": "string",
"size": "string",
"amount": 0,
"fullPrice": 0.0,
"price": 0.0,
"total": 0.0,
"url": "string",
"fromRecommendation": false,
"cartItemId": 0
}
],
"paymentMethods": {
"creditCard": true,
"pix": { "operator": 0, "key": "string", "discountPercentage": null, "discountValue": null },
"boleto": false,
"installments": [
{ "installment": "1", "value": 0.0, "description": "1x de R$ 0,00" }
],
"isTwoCreditCardsAllowed": false
},
"recommendations": {
"hoursToExpire": "0:00",
"items": [
{
"recommendedItemId": 0,
"productId": 0,
"sku": "string",
"productName": "string",
"description": "string",
"thumbnail": "string",
"images": [],
"fullPrice": 0.0,
"discountValue": 0.0,
"discountType": 0,
"size": "string"
}
]
}
}
Responses 401
| Cenário | Mensagem |
|---|---|
Header api-company-target ausente ou vazio | "Schema invalid" |
| Token JWT inválido ou ausente | (401 padrão do ASP.NET) |
Responses 429
| Cenário | Mensagem |
|---|---|
| Lock não adquirido (concorrência) | "Pagamento em processamento." |
Responses 400
| Cenário | Mensagem |
|---|---|
| Payment não encontrado | "Payment não encontrado" |
| Payment sem carrinho associado | "Pagamento não possui carrinho associado" |
| Status do payment inválido | "Não foi possível remover o item recomendado do pedido." |
Cart com SaleEcommerce == true | "Operação não disponível para este tipo de pedido" |
cartItemId não encontrado no cart | "Item não encontrado no carrinho" |
Item com FromRecommendation == false | "Item não removível" |
| Erro da Cart API (propagado) | Mensagens da Cart API ou "Não foi possível remover o item" |
| Erro ao persistir Payment.Total | "Erro interno ao atualizar o pagamento." |
Dependências
| Dependência | Referência | Status |
|---|---|---|
Cart API — endpoint DELETE api/cart/{cartId}/items/{cartItemId} | Task 194883 — Cart API: Remove Item | Implementado |
Flag FromRecommendation em CartItemsModel | CartItemsModel.cs:57 (FromRecommendation bool, coluna "FromRecommendation") | Disponível |
cartItemId exposto no Product DTO | ADR-008 · GetInfoBase.cs (Product.CartItemId) | Implementado |
CartIntegrationService.RemoveRecommendedItemAsync | A ser implementado (novo método) | Pendente |
RemoveRecommendedItemCommand | A ser implementado (novo command) | Pendente |
RemoveRecommendedItemCommandHandler | A ser implementado (novo handler) | Pendente |
RemoveRecommendedItemResponseDTO | A ser implementado (mesmo shape do AddRecommendedItemResponseDTO) | Pendente |
Lock distribuído (ILockService) | Já utilizado em AddRecommendedItemCommandHandler | Disponível |
LogQueueService + LogQueueMessageEvent | Já utilizado em AddRecommendedItemCommandHandler | Disponível |
Sem
RemoveRecommendedItemValidator: Não será criado validator FluentValidation. A validação decartItemIdé feita no handler (busca emcart.ItemsporFirstOrDefault(i => i.Id == command.CartItemId)). O ASP.NET model binding já garante quecartItemIdé umintválido via route constraint{cartItemId:int}. Se não-numérico, a request é rejeitada antes de chegar ao handler. Esta decisão elimina complexidade desnecessária (decorre do design RESTful com route param).
Fora de escopo
- Lógica interna da Cart API (soft-delete, desvinculação de recomendação, recálculo de totais) — responsabilidade da Task 194883
- Adição de item recomendado — Task 194880 — Add Recommended Item
- Validação de janela de validade (1h) — não se aplica ao remove; o item já foi aceito e está no cart
Questões abertas
Nenhuma.