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 (
PaymentsModeldeLink.OrgDB.Entities). - Não atende: pagamentos Ecommerce (
PaymentsEcommModel) — rejeitados na validação.
Decisões de Design
- Command (não Query): a operação modifica estado (adiciona item ao carrinho via API interna e atualiza
Payment.Total), portanto é modelada comoICommand<BaseResult>. - 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 deILockService.LockAsync/ReleaseLockAsyncem try/finally para evitar race condition sobre o mesmo pagamento. - Serviço dedicado para a API interna de carrinho: a chamada HTTP para adicionar o item é encapsulada em
ICartIntegrationService, usandoHttpClientregistrado no DI com Polly retry. O endpoint alvo será criado em outra tarefa. - Reutilização de DTOs existentes:
Values,Product(com adição deCartItemId),PaymentMethods,InstallmentseRecommendationsInfoDTO/RecommendedItemsInfoDTOdoGetInfoBase/GetInfosão reutilizados. ApenasAddRecommendedItemResponseDTOé novo, agregando os DTOs existentes na resposta. - Acesso a recomendações via
ICoreSqlRepository.GetRecommendedItemsAsync: o commit #194879 introduziuICoreSqlRepository.GetRecommendedItemsAsync(int cartId)que retornaList<RecommendedItemsInfoDTO>via SQL Dapper (join comCartItemRecommendations+Products+ProductPhotos). Não estenderICartRepository— usar o método já existente emICoreSqlRepository. ParaSaleEcommerceeDateCreated, usarICoreSqlRepository.GetCartInfoAsync(int cartId)que retornaCartInfoDTOcom essas propriedades. - Validação Store vs Ecommerce via
CartInfoDTO.SaleEcommerce:PaymentsModelNÃO possui propriedadeType/PaymentType. A validação é feita viaCartInfoDTO.SaleEcommerce == false(Store). SeSaleEcommerce == true, rejeitar. - Janela de expiração: validação usa a constante
RECOMMENDATION_EXPIRATION_MINUTES = 60, seguindo o padrão já estabelecido emGetInfoQueryHandler. Verificacart.DateCreated.AddMinutes(RECOMMENDATION_EXPIRATION_MINUTES) >= now.
Architecture
Diagrama de Fluxo
Camadas e Responsabilidades
| Camada | Componente | Responsabilidade |
|---|---|---|
| API | PaymentController | Recebe request, seta TransactionId, despacha command via mediator |
| API | AddRecommendedItemCommand | DTO do command com PaymentGuid e RecommendedItemId |
| API | AddRecommendedItemValidator | FluentValidation do command |
| API | AddRecommendedItemCommandHandler | Orquestração: validações, lock, chamada Cart API, atualização, resposta |
| Domain | ICoreSqlRepository (existente) | GetCartInfoAsync(cartId) para SaleEcommerce/DateCreated; GetRecommendedItemsAsync(cartId) para recomendações |
| Domain | ICartIntegrationService (novo) | Contrato para chamada HTTP ao coezzion-service-cart |
| Domain | AddRecommendedItemResponseDTO | DTO de resposta (agrega DTOs existentes) |
| Infrastructure | CartIntegrationService (novo) | HttpClient + Polly para coezzion-service-cart |
Components and Interfaces
Novos Arquivos
| Arquivo | Camada | Tipo |
|---|---|---|
src/Checkout.API/Application/Messages/Commands/RecommendedItemCommands/AddRecommendedItemCommand.cs | API | Command |
src/Checkout.API/Application/Messages/Commands/RecommendedItemCommands/AddRecommendedItemCommandHandler.cs | API | Handler |
src/Checkout.API/Application/Validators/RecommendedItem/AddRecommendedItemValidator.cs | API | Validator |
src/Checkout.Domain/DTO/RecommendedItem/AddRecommendedItemResponseDTO.cs | Domain | DTO |
src/Checkout.Domain/Interfaces/Services/Cart/ICartIntegrationService.cs | Domain | Interface |
src/Checkout.Infraestructure/Integrations/Cart/CartIntegrationService.cs | Infrastructure | Service |
Arquivos Modificados
| Arquivo | Modificação |
|---|---|
src/Checkout.API/Controllers/PaymentController.cs | Novo endpoint AddRecommendedItem |
src/Checkout.Domain/DTO/GetInfo/GetInfoBase.cs | Adicionar prop CartItemId no record Product |
src/Checkout.API/Configuration/DependencyInjectionConfig.cs | Registro de ICartIntegrationService + HttpClient + Polly |
Nota:
ICoreSqlRepositoryeCoreSqlRepositoryjá 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:
Productreceberá uma nova propriedadeCartItemId(int) para que o front possa referenciar o item no fluxo de remoção (RemoveRecommendedItem).CartItemIdé mapeado a partir deCartItemsInfoDTO.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ário | Status | Mensagem |
|---|---|---|
| Token ausente/inválido | 401 | (sem body - middleware) |
| GUID inválido na rota | 400 | "Payment não encontrado" |
| Payment não encontrado | 400 | "Payment não encontrado" |
| Lock não adquirido (race) | 429 | "Pagamento em processamento" |
| Cart não encontrado | 400 | "Carrinho não encontrado" |
CartInfoDTO.SaleEcommerce == true | 400 | "Operação não disponível para este tipo de pedido" |
| CartId nulo/zero | 400 | "Pagamento não possui carrinho associado" |
| Recomendação não encontrada | 400 | "Recomendação não encontrada" |
| Recomendação já efetivada (CartItemId != null) | 400 | "Recomendação já adicionada ao carrinho" |
| Janela expirada | 400 | "Recomendação não está mais disponível" |
| Produto sem estoque | 400 | "Produto sem estoque na loja" |
| Erro genérico Cart API | 400 | "Não foi possível adicionar o item" |
| Timeout Cart API | 400 | "Não foi possível adicionar o item" |
| Falha persistência | 500 | "Erro interno ao atualizar o pagamento" |
| Falha montagem DTO | 500 | "Erro interno ao montar a resposta" |
recommendedItemId <= 0 | 400 | "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)
| Property | Tag |
|---|---|
| 1 — Time window validation | Feature: add-recommended-item, Property 1 |
| 2 — Cart API error categorization | Feature: 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-positive | Feature: add-recommended-item, Property 5 |
Dependências Externas
| Dependência | Status |
|---|---|
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 |