Skip to main content

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:

  1. WHEN o cliente chama DELETE api/payment/v2/{id:guid}/items/{cartItemId:int} THEN o sistema SHALL exigir autenticação via [Authorize(AuthenticationSchemes = "PaymentsScheme")] e validação de schema via [AddSchema] (header api-company-target obrigatório). O cartItemId é informado na rota (route param), não no body — o ASP.NET model binding garante que é um int válido via constraint {cartItemId:int}.
  2. WHEN o {id} (GUID do payment) não corresponde a nenhum payment THEN o sistema SHALL retornar 400 com mensagem "Payment não encontrado".
  3. WHEN o payment encontrado tem CartId <= 0 THEN o sistema SHALL retornar 400 com mensagem "Pagamento não possui carrinho associado".
  4. WHEN o payment encontrado tem Status diferente de Created ou CreateTransactionFail THEN o sistema SHALL registrar na alllogs com prefixo [Invalido] ("[Invalido] Tentativa de remover item recomendado {cartItemId} do carrinho {cartId} - {status}") e retornar 400 com mensagem "Não foi possível remover o item recomendado do pedido.".
  5. WHEN o payment encontrado tem Status igual a Created ou CreateTransactionFail THEN o sistema SHALL prosseguir com o processamento.
  6. WHEN o cart vinculado ao payment tem SaleEcommerce == true THEN o sistema SHALL retornar 400 com 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) Status inválido (RF-01.4) → (4) busca cart e valida SaleEcommerce → (5) item em cart.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 SaleEcommerce ocorre em duas camadas: (1) no checkout service, que busca o CartInfoDTO via GetCartInfoAsync e valida cart.SaleEcommerce; (2) na Cart API, que valida cart.SaleEcommerce no RemoveCartItemHandler. A duplicação é intencional para proteger contra chamadas diretas à Cart API.


RF-02 — Validar o item a remover

Acceptance Criteria:

  1. WHEN o cartItemId informado não corresponde a nenhum CartItem na coleção cart.Items (via CartItemsInfoDTO) THEN o sistema SHALL retornar 400 com mensagem "Item não encontrado no carrinho".
  2. WHEN o CartItem encontrado tem FromRecommendation == false THEN o sistema SHALL retornar 400 com mensagem "Item não removível".

RF-03 — Chamar a Cart API para remover o item

Acceptance Criteria:

  1. WHEN todas as validações anteriores passam THEN o sistema SHALL chamar DELETE api/cart/{cartId}/items/{cartItemId} via CartIntegrationService.RemoveRecommendedItemAsync(cartId, cartItemId).
  2. WHEN a Cart API retorna sucesso (HTTP 200) THEN o sistema SHALL prosseguir para a atualização do Payment.Total.
  3. WHEN o CartIntegrationService retorna erros (falha de rede, timeout, exceção, ou erros de negócio propagados da Cart API) THEN o sistema SHALL registrar a falha na alllogs com detalhes e retornar os erros propagados via ReturnErrors(cartResult.Errors).

Mensagem genérica de erro: O CartIntegrationService converte todas as exceções (timeout, conexão, erro HTTP) em BaseResult.Errors com 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:

  1. WHEN a Cart API confirma sucesso THEN o sistema SHALL buscar o cart atualizado via GetCartInfoAsync(cartId) para obter o novo total.
  2. WHEN o Payment.Total é diferente do updatedCart.Total THEN o sistema SHALL atualizar sincronamente o campo Total do PaymentModel com o novo total e persistir via Update + SaveAsync.
  3. WHEN a atualização do Payment.Total falha com exceção THEN o sistema SHALL registrar a falha na alllogs (mensagem truncada em 512 chars) e retornar 400 com mensagem "Erro interno ao atualizar o pagamento.".
  4. WHEN o Payment.Total é atualizado com sucesso THEN o sistema SHALL retornar 200 com o RemoveRecommendedItemResponseDTO contendo:
    • valuesValues recalculado com o novo total (construtor new Values(updatedCart))
    • productsList<Product> completa e atualizada dos CartItems (sem o item removido, cada item com cartItemId)
    • paymentMethodsPaymentMethods com parcelas recalculadas para o novo total
    • recommendationsRecommendationsInfoDTO? (wrapper com HoursToExpire e Items filtrados por CartItemId == null; recomendações anteriormente vinculadas ao item removido voltam a ficar disponíveis após o desvinculamento)

Após a remoção, o CartItemRecommendationModel tem CartItemId setado para null pela Cart API (RevertRecommendedItem), o que faz a recomendação reaparecer na lista de disponíveis (BuildAvailableRecommendations filtra por CartItemId == null).


RF-05 — Auditoria (alllogs)

Acceptance Criteria:

  1. WHEN o handler inicia o processamento após validação de lock THEN o sistema SHALL registrar na alllogs "Tentativa de remover item recomendado {cartItemId} do carrinho {cartId}" com método "Handle-RemoveRecommendedItem".
  2. WHEN a Cart API retorna erros THEN o sistema SHALL registrar na alllogs "Erro ao chamar Cart API para remover item recomendado: {erros_concatenados}" com método "Handle-RemoveRecommendedItem".
  3. WHEN o Payment.Total muda após a remoção THEN o sistema SHALL registrar na alllogs "Valor do pedido mudou: {last} -> {new}" com método "Handle-RemoveRecommendedItem".
  4. WHEN ocorre exceção ao persistir o payment THEN o sistema SHALL registrar 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:

  1. WHEN o handler inicia o processamento THEN o sistema SHALL adquirir lock distribuído via ILockService.LockAsync com chave "rec-item:{PaymentGuid}" e TTL de 10 segundos.
  2. WHEN o lock não pode ser adquirido (concorrência) THEN o sistema SHALL retornar HTTP 429 com mensagem "Pagamento em processamento.".
  3. WHEN o handler termina (sucesso ou erro) THEN o sistema SHALL liberar o lock via ILockService.ReleaseLockAsync.

Lock key compartilhada com ADD: A chave "rec-item:{PaymentGuid}" é a mesma usada pelo AddRecommendedItemCommandHandler, 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} é int via 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árioMensagem
Header api-company-target ausente ou vazio"Schema invalid"
Token JWT inválido ou ausente(401 padrão do ASP.NET)

Responses 429

CenárioMensagem
Lock não adquirido (concorrência)"Pagamento em processamento."

Responses 400

CenárioMensagem
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ênciaReferênciaStatus
Cart API — endpoint DELETE api/cart/{cartId}/items/{cartItemId}Task 194883 — Cart API: Remove ItemImplementado
Flag FromRecommendation em CartItemsModelCartItemsModel.cs:57 (FromRecommendation bool, coluna "FromRecommendation")Disponível
cartItemId exposto no Product DTOADR-008 · GetInfoBase.cs (Product.CartItemId)Implementado
CartIntegrationService.RemoveRecommendedItemAsyncA ser implementado (novo método)Pendente
RemoveRecommendedItemCommandA ser implementado (novo command)Pendente
RemoveRecommendedItemCommandHandlerA ser implementado (novo handler)Pendente
RemoveRecommendedItemResponseDTOA ser implementado (mesmo shape do AddRecommendedItemResponseDTO)Pendente
Lock distribuído (ILockService)Já utilizado em AddRecommendedItemCommandHandlerDisponível
LogQueueService + LogQueueMessageEventJá utilizado em AddRecommendedItemCommandHandlerDisponível

Sem RemoveRecommendedItemValidator: Não será criado validator FluentValidation. A validação de cartItemId é feita no handler (busca em cart.Items por FirstOrDefault(i => i.Id == command.CartItemId)). O ASP.NET model binding já garante que cartItemId é um int vá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.