Skip to main content

Design Document

Overview

Este design define a arquitetura e componentes necessários para implementar o endpoint POST api/payment/v2/{id}/add-recommended-item no coezzion-service-checkout. O endpoint permite que um cliente no zzlink adicione um item recomendado pelo vendedor ao seu pedido Store antes de finalizar o pagamento.

O fluxo principal segue o padrão CQRS existente no projeto: Controller → Command → Handler → Services/Repositories, usando ZZMediator para orquestração. O handler coordena validações locais, lock distribuído, chamada à API interna do coezzion-service-cart e atualização do pagamento, retornando um DTO consolidado com os dados atualizados do pedido.

Escopo de tipo de pagamento

  • Atende: pagamentos Store (PaymentsModel de Link.OrgDB.Entities).
  • Não atende: pagamentos Ecommerce (PaymentsEcommModel) — rejeitados na validação.

Decisões de Design

  1. Command (não Query): a operação modifica estado (adiciona item ao carrinho via API interna e atualiza Payment.Total), portanto é modelada como ICommand<BaseResult>.
  2. Handler único com lock distribuído: toda a orquestração (validação, chamada externa, persistência, montagem de resposta) ocorre no handler, seguindo o padrão de CreatePaymentCommandHandler. Obrigatório uso de ILockService.LockAsync/ReleaseLockAsync em try/finally para evitar race condition sobre o mesmo pagamento.
  3. Serviço dedicado para a API interna de carrinho: a chamada HTTP para adicionar o item é encapsulada em ICartIntegrationService, usando HttpClient registrado no DI com Polly retry. O endpoint alvo será criado em outra tarefa.
  4. Reutilização de DTOs existentes: Values, Product (com adição de CartItemId), PaymentMethods, Installments e RecommendationsInfoDTO/RecommendedItemsInfoDTO do GetInfoBase/GetInfo são reutilizados. Apenas AddRecommendedItemResponseDTO é novo, agregando os DTOs existentes na resposta.
  5. Acesso a recomendações via ICoreSqlRepository.GetRecommendedItemsAsync: o commit #194879 introduziu ICoreSqlRepository.GetRecommendedItemsAsync(int cartId) que retorna List<RecommendedItemsInfoDTO> via SQL Dapper (join com CartItemRecommendations + Products + ProductPhotos). Não estender ICartRepository — usar o método já existente em ICoreSqlRepository. Para SaleEcommerce e DateCreated, usar ICoreSqlRepository.GetCartInfoAsync(int cartId) que retorna CartInfoDTO com essas propriedades.
  6. Validação Store vs Ecommerce via CartInfoDTO.SaleEcommerce: PaymentsModel NÃO possui propriedade Type/PaymentType. A validação é feita via CartInfoDTO.SaleEcommerce == false (Store). Se SaleEcommerce == true, rejeitar.
  7. Janela de expiração: validação usa a constante RECOMMENDATION_EXPIRATION_MINUTES = 60, seguindo o padrão já estabelecido em GetInfoQueryHandler. Verifica cart.DateCreated.AddMinutes(RECOMMENDATION_EXPIRATION_MINUTES) >= now.

Architecture

Diagrama de Fluxo

Camadas e Responsabilidades

CamadaComponenteResponsabilidade
APIPaymentControllerRecebe request, seta TransactionId, despacha command via mediator
APIAddRecommendedItemCommandDTO do command com PaymentGuid e RecommendedItemId
APIAddRecommendedItemValidatorFluentValidation do command
APIAddRecommendedItemCommandHandlerOrquestração: validações, lock, chamada Cart API, atualização, resposta
DomainICoreSqlRepository (existente)GetCartInfoAsync(cartId) para SaleEcommerce/DateCreated; GetRecommendedItemsAsync(cartId) para recomendações
DomainICartIntegrationService (novo)Contrato para chamada HTTP ao coezzion-service-cart
DomainAddRecommendedItemResponseDTODTO de resposta (agrega DTOs existentes)
InfrastructureCartIntegrationService (novo)HttpClient + Polly para coezzion-service-cart

Components and Interfaces

Novos Arquivos

ArquivoCamadaTipo
src/Checkout.API/Application/Messages/Commands/RecommendedItemCommands/AddRecommendedItemCommand.csAPICommand
src/Checkout.API/Application/Messages/Commands/RecommendedItemCommands/AddRecommendedItemCommandHandler.csAPIHandler
src/Checkout.API/Application/Validators/RecommendedItem/AddRecommendedItemValidator.csAPIValidator
src/Checkout.Domain/DTO/RecommendedItem/AddRecommendedItemResponseDTO.csDomainDTO
src/Checkout.Domain/Interfaces/Services/Cart/ICartIntegrationService.csDomainInterface
src/Checkout.Infraestructure/Integrations/Cart/CartIntegrationService.csInfrastructureService

Arquivos Modificados

ArquivoModificação
src/Checkout.API/Controllers/PaymentController.csNovo endpoint AddRecommendedItem
src/Checkout.Domain/DTO/GetInfo/GetInfoBase.csAdicionar prop CartItemId no record Product
src/Checkout.API/Configuration/DependencyInjectionConfig.csRegistro de ICartIntegrationService + HttpClient + Polly

Nota: ICoreSqlRepository e CoreSqlRepository já possuem os métodos necessários (GetCartInfoAsync, GetRecommendedItemsAsync) introduzidos no commit #194879. Nenhuma modificação adicional é necessária nesses arquivos.

Interfaces

// ICoreSqlRepository — MÉTODOS JÁ EXISTENTES (commit #194879)
public interface ICoreSqlRepository
{
// ... métodos existentes ...
Task<CartInfoDTO> GetCartInfoAsync(int cartId); // JÁ EXISTE
Task<List<RecommendedItemsInfoDTO>> GetRecommendedItemsAsync(int cartId); // JÁ EXISTE
}

// Novo serviço de integração com coezzion-service-cart
public interface ICartIntegrationService
{
Task<BaseResult> AddRecommendedItemAsync(int cartId, int recommendationId);
}

Data Models

AddRecommendedItemCommand

public class AddRecommendedItemCommand : ICommand<BaseResult>
{
public Guid PaymentGuid { get; set; }
public int RecommendedItemId { get; set; } // CartItemRecommendationModel.Id
}

AddRecommendedItemResponseDTO

public record AddRecommendedItemResponseDTO
{
public Values Values { get; init; }
public List<Product> Products { get; init; }
public PaymentMethods PaymentMethods { get; init; }
public RecommendationsInfoDTO? Recommendations { get; init; } // Reutiliza DTO existente
}

Nota: Product receberá uma nova propriedade CartItemId (int) para que o front possa referenciar o item no fluxo de remoção (RemoveRecommendedItem). CartItemId é mapeado a partir de CartItemsInfoDTO.Id.

RecommendedItemsInfoDTO (JÁ EXISTE — commit #194879)

public record RecommendedItemsInfoDTO
{
public int RecommendedItemId { get; set; }
public int ProductId { get; set; }
public string Sku { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
public string? Thumbnail { get; set; }
public List<string> Images { get; set; } = new();
public decimal FullPrice { get; set; }
public decimal DiscountValue { get; set; }
public DiscountType DiscountType { get; set; }
public string Size { get; set; }

[JsonIgnore]
public int? CartItemId { get; set; }
}

RecommendationsInfoDTO (JÁ EXISTE — commit #194879)

public record RecommendationsInfoDTO
{
public string HoursToExpire { get; set; } = "0:00";
public List<RecommendedItemsInfoDTO> Items { get; set; } = [];
}

CartInfoDTO — campos relevantes (JÁ EXISTE)

public record CartInfoDTO
{
public int Id { get; set; }
public bool SaleEcommerce { get; set; }
public decimal Total { get; set; }
public DateTime DateCreated { get; set; }
public RecommendationsInfoDTO? Recommendations { get; set; } = null;
// ... demais campos ...
}

Entidade CartItemRecommendationModel (Core.OrgDB — JÁ EXISTE)

public class CartItemRecommendationModel : BaseEntity
{
public int CartId { get; set; }
public int ProductId { get; set; }
public int? CartItemId { get; set; } // FK quando recomendação virou item
public string Size { get; set; }
public DiscountType DiscountType { get; set; } // None=0, Percentage=1, Value=2
public decimal DiscountValue { get; set; }
public decimal UnitPrice { get; set; }
public decimal FullPrice { get; set; }
public bool HasEmployeeDiscount { get; set; }
// Navigation Properties
public CartModel Cart { get; set; }
public ProductModel Product { get; set; }
public CartItemsModel CartItem { get; set; }
}

Entidade PaymentsModel (Link.OrgDB — JÁ EXISTE)

public class PaymentsModel : BaseEntity, IAggregateRoot
{
public int CartId { get; set; }
public PaymentStatus Status { get; set; }
public DateTime DateExpiration { get; set; }
public decimal Total { get; set; }
public Guid PaymentGuid { get; set; }
public string TransactionId { get; set; }
// NOTA: NÃO possui propriedade Type/PaymentType
}

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: Time window validation is consistent

For any CartInfoDTO.DateCreated value and current time now, the recommendation validity check SHALL accept the operation if and only if CartInfoDTO.DateCreated.AddMinutes(RECOMMENDATION_EXPIRATION_MINUTES) >= now, where RECOMMENDATION_EXPIRATION_MINUTES = 60, and SHALL reject with "Recomendação não está mais disponível" otherwise.

Validates: Requirements 3.3

Property 2: Cart API non-success error categorization

For any non-success response da API interna do coezzion-service-cart que não seja especificamente um erro de out-of-stock, o handler SHALL retornar "Não foi possível adicionar o item" como mensagem de erro.

Validates: Requirements 4.4, 4.5

Property 3: Response DTO completeness on success

For any successful execution, the response SHALL contain non-null values, products, paymentMethods, and recommendations fields, where recommendations.Items contém apenas recomendações não efetivadas (RecommendedItemsInfoDTO.CartItemId == null). Cada Product em products SHALL conter cartItemId para referência no fluxo de remoção.

Validates: Requirements 5.2

Property 4: Log message truncation

For any error description string, the log message SHALL have length <= 512 characters.

Validates: Requirements 6.3

Property 5: Input validation rejects non-positive recommendedItemId

For any integer value <= 0 provided as recommendedItemId, the validator SHALL reject the request.

Validates: Requirements 7.2

Error Handling

Tabela de Erros

CenárioStatusMensagem
Token ausente/inválido401(sem body - middleware)
GUID inválido na rota400"Payment não encontrado"
Payment não encontrado400"Payment não encontrado"
Lock não adquirido (race)429"Pagamento em processamento"
Cart não encontrado400"Carrinho não encontrado"
CartInfoDTO.SaleEcommerce == true400"Operação não disponível para este tipo de pedido"
CartId nulo/zero400"Pagamento não possui carrinho associado"
Recomendação não encontrada400"Recomendação não encontrada"
Recomendação já efetivada (CartItemId != null)400"Recomendação já adicionada ao carrinho"
Janela expirada400"Recomendação não está mais disponível"
Produto sem estoque400"Produto sem estoque na loja"
Erro genérico Cart API400"Não foi possível adicionar o item"
Timeout Cart API400"Não foi possível adicionar o item"
Falha persistência500"Erro interno ao atualizar o pagamento"
Falha montagem DTO500"Erro interno ao montar a resposta"
recommendedItemId <= 0400"O campo recommendedItemId deve ser um número inteiro positivo"

Handler Pattern

private const int RECOMMENDATION_EXPIRATION_MINUTES = 60;

public async Task<BaseResult> Handle(AddRecommendedItemCommand command, CancellationToken cancellationToken)
{
var payment = await _paymentRepository.GetByGuidPaymentAsync(command.PaymentGuid);
if (payment is null)
return WithError("Payment não encontrado.");

var lockKey = $"add-rec-item:{payment.Id}";
try
{
var locked = await _lockService.LockAsync(lockKey, payment.Id.ToString(), TimeSpan.FromSeconds(10));
if (!locked)
return BaseResult.WithTooManyRequest("Pagamento em processamento.");

// Usa ICoreSqlRepository existente (commit #194879)
var cart = await _coreSqlRepository.GetCartInfoAsync(payment.CartId);
if (cart is null)
return WithError("Carrinho não encontrado.");
if (cart.SaleEcommerce)
return WithError("Operação não disponível para este tipo de pedido.");

var recommendedItems = await _coreSqlRepository.GetRecommendedItemsAsync(payment.CartId);
var recommendation = recommendedItems.FirstOrDefault(r => r.RecommendedItemId == command.RecommendedItemId);
if (recommendation is null)
return WithError("Recomendação não encontrada.");
if (recommendation.CartItemId != null)
return WithError("Recomendação já adicionada ao carrinho.");

var expirationTime = cart.DateCreated.AddMinutes(RECOMMENDATION_EXPIRATION_MINUTES);
if (DateTime.Now.ToBrazillianTime() > expirationTime)
return WithError("Recomendação não está mais disponível.");

// ... chamada à Cart API, update do Total ...

var dto = await BuildResponseAsync(cart, payment, recommendedItems);
return BaseResult.With(dto);
}
catch (Exception ex)
{
_ = _logQueueService.EnqueueAsync(
LogQueueMessageEvent.SaveLogOrderHistoryMessage(
_userProvider.GetSchemaName(),
payment.Id,
$"Erro ao adicionar item recomendado: {ex.Message.Truncate(512)}"));
return WithError("Erro interno ao atualizar o pagamento.");
}
finally
{
await _lockService.ReleaseLockAsync(lockKey, payment.Id.ToString());
}
}

Testing Strategy

Testes Unitários do Handler (xUnit + Moq)

  • Payment não encontrado → erro
  • Lock não adquirido → 429
  • Cart não encontrado → erro
  • Cart.SaleEcommerce == true → erro
  • Recomendação não encontrada → erro
  • Recomendação já efetivada (CartItemId != null) → erro
  • Janela expirada → erro
  • Cart API sucesso → atualiza Total e retorna DTO
  • Cart API sem estoque → erro específico
  • Cart API erro genérico → erro genérico
  • Falha persistência → erro 500
  • Lock release sempre executado (finally)
  • LogQueueService chamado nos caminhos de erro

Testes de Propriedade (FsCheck.Xunit, 100+ iterações)

PropertyTag
1 — Time window validationFeature: add-recommended-item, Property 1
2 — Cart API error categorizationFeature: add-recommended-item, Property 2
3 — Response DTO completeness (CartItemId != null)Feature: add-recommended-item, Property 3
4 — Log message truncation (≤ 512 chars)Feature: add-recommended-item, Property 4
5 — Input validation rejects non-positiveFeature: add-recommended-item, Property 5

Dependências Externas

DependênciaStatus
coezzion-service-cart — endpoint AddItem🔴 TBD (outra tarefa)
Contrato de erro "out_of_stock"🔴 TBD
CartItemRecommendationModel (Core.OrgDB)🟢 disponível
ICoreSqlRepository.GetRecommendedItemsAsync🟢 disponível (commit #194879)
ICoreSqlRepository.GetCartInfoAsync🟢 disponível (commit #194879)
RecommendedItemsInfoDTO🟢 disponível (commit #194879)
RecommendationsInfoDTO🟢 disponível (commit #194879)
CartInfoDTO.SaleEcommerce / CartInfoDTO.DateCreated🟢 disponível (commit #194879)
ILockService🟢 disponível
ILogQueueService🟢 disponível
Multitenancy (IUserProvider.GetSchemaName())🟢 disponível