Skip to main content

Plano de Refatoração — Histórico de Recomendações (Cart API)

US: 195746 — Registro de eventos/indicadores do Upsell
Data: 2026-07-01
Status: Proposta para análise — sem implementação
Premissa aceita: adicionar RecommendationsId (Guid?) em CartModel e popular no create.


1. Objetivo

Gravar histórico de três eventos correlacionados por RecommendationsId:

EventoEnumTrigger
Carrinho criado com upsellRecommendationCartCreatedPOST api/cart
Produto recomendado adicionadoRecommendationProductAddedToOrderAdd item recomendação
Produto recomendado removidoRecommendationProductRemovedToOrderRemove item recomendação

Pipeline assíncrono (mantido):

API publica → SQS FIFO → Listener → CartRecommendationHistoryService → DB

2. Decisões arquiteturais

2.1 Domain events — descartados

Ver context/domain-events-analysis.md.

Resumo: CartModel herda BaseEntity, mas CoreOrgDbContext não despacha domain events. Adotar exigiria alterar pacote compartilhado coezzion-db-core e acoplar entidade a contexto HTTP. Over desnecessário.

2.2 Event Handler / Publisher explícito — adotado

Ver context/event-handlers-pattern.md e context/log-queue-reference.md.

Resumo: criar ICartRecommendationHistoryPublisher seguindo padrão ILogQueueService (Checkout AllLogs) — interface em Domain, implementação em Infrastructure, chamada explícita nos pontos de negócio.

2.3 RecommendationsId no CartModel — adotado

Elimina query LastForCartId e race condition com listener SQS. Fonte de verdade síncrona no mesmo aggregate já carregado em add/remove.

2.4 Fire-and-forget — mantido (com correção de forma)

Requisito RF-02 (task #196022): falha de observabilidade não impacta criação do carrinho.

Errado (atual)Correto (proposto)
_ = SendFifoAsync(...) sem logTask.Run + await interno + LogError no catch
Bloquear response com await no hot pathCaller não aguarda o Task do publisher

3. Arquitetura proposta


4. Mudanças por camada

4.1 DbCore (coezzion-db-core)

4.1.1 CartModel

Arquivo: src/Core.OrgDB/Entities/CartModel.cs

public Guid? RecommendationsId { get; set; }
  • Nullable — carrinhos sem fluxo upsell permanecem null
  • Sem alteração de construtores obrigatória (property setter)
  • Colocar junto a TransactionId e ShowCaseId (campos de correlação)

4.1.2 Migration EF

  • Nova migration: CartModel_Add_RecommendationsId
  • Coluna: uuid NULL (PostgreSQL) / uniqueidentifier NULL
  • Índice: não necessário no primeiro momento (não há query por RecommendationsId no cart — só leitura após load por CartId)

4.1.3 Backfill

  • Carrinhos existentes: NULL — correto (não eram upsell)
  • Sem script de backfill obrigatório

4.2 Cart.Domain

4.2.1 Interface do publisher

Novo: Interfaces/Services/ICartRecommendationHistoryPublisher.cs

public interface ICartRecommendationHistoryPublisher
{
Task PublishCreateCartAsync(
string schema,
Guid recommendationsId,
CartModel cart,
string responseJson);

Task PublishAddItemAsync(
string schema,
Guid recommendationsId,
CartModel cart,
AddItemToCartCommand command,
object response,
int statusCode = 200);

Task PublishRemoveItemAsync(
string schema,
Guid recommendationsId,
CartModel cart,
RemoveCartItemCommand command,
object response,
int statusCode = 200);
}

Por que 3 métodos e não 1 genérico?

  • Clareza nos call sites — cada operação tem parâmetros distintos
  • Factories CartRecommendationsEvent.FromCreate* já separadas
  • Sem over-abstração de builder genérico

4.2.2 DTO de response enxuto

Novo: DTO/CartRecommendationHistoryResponseDTO.cs

public record CartRecommendationHistoryResponseDTO(
int CartId,
decimal Total,
IReadOnlyList<int>? ItemIds = null);

Usado no lugar de serializar CartModel inteiro (PII, tamanho, ciclos de referência).

4.2.3 Remover LastForCartId (opcional pós-refatoração)

Arquivo: Interfaces/Repositories/ICartRecommendationHistoryRepository.cs

Remover:

Task<Guid?> LastForCartId(int cartId);

Se não houver outro consumidor. Repository fica só com Add + SaveAsync (persistência do listener).


4.3 Cart.Infrastructure

4.3.1 Implementação do publisher

Novo: Services/CartRecommendationHistoryPublisher.cs

Dependências (primary constructor):

  • IMessageBusServiceCachedDecorator _messageBusService
  • ILogger<CartRecommendationHistoryPublisher> _logger

Lógica central (privada):

private Task EnqueueAsync(CartRecommendationsEvent evt, Guid recommendationsId)
{
return Task.Run(async () => { /* padrão LogQueueService */ });
}

Métodos públicos:

  1. Montam CartRecommendationsEvent via factories existentes
  2. CodeStore = cart.Store?.CodeStore ?? string.Empty
  3. Chamam EnqueueAsync

JSON serialization: manter ReferenceHandler.IgnoreCycles apenas onde necessário (commands). Response usa DTO enxuto.

4.3.2 CartRecommendationHistoryRepository

Remover implementação de LastForCartId se interface for limpa.


4.4 Cart.API

Arquivos:

  • Handlers/CreateCart/CommandToCartModelHandler.cs
  • Handlers/CreateCart/CommandToCartModelPortalHandler.cs

Após new CartModel(...):

cart.RecommendationsId = command.RecommendationsId;

4.4.2 Edge case — idempotência por TransactionId

Arquivo: Handlers/CreateCart/CartTransactionIdHandler.cs

Quando reutiliza carrinho existente (cartTransaction != null):

if (command.RecommendationsId.HasValue && cartTransaction.RecommendationsId is null)
cartTransaction.RecommendationsId = command.RecommendationsId;

Garante que retry de create com mesmo TransactionId propaga o ID para add/remove futuros. O SaveCartHandler persistirá se Id != 0... verificar: hoje SaveCartHandler só salva quando Id == 0. Para update do RecommendationsId em cart existente, pode ser necessário:

  • Opção A: SaveCartHandler também chama Update + SaveAsync quando RecommendationsId mudou em cart existente
  • Opção B: update inline no CartTransactionIdHandler via repository

Recomendação: Opção A no SaveCartHandler — condição mínima:

if (context.CartModel is { Id: > 0, RecommendationsId: not null } cart
&& cart.RecommendationsId != /* valor original */)
{
cartRepository.Update(cart);
await cartRepository.SaveAsync();
}

Ou simplificar: se CartModel.Id > 0 e command.RecommendationsId presente, sempre Update (idempotente).

4.4.3 PublishRecommendationHistoryHandler — simplificar

Arquivo: Handlers/CreateCart/PublishRecommendationHistoryHandler.cs

AntesDepois
Injeta IMessageBusServiceCachedDecoratorInjeta ICartRecommendationHistoryPublisher
Monta event + _ = SendFifoAsync_publisher.PublishCreateCartAsync(...)
CreateResponse(context)Mantém — método pode ficar no handler ou mover para DTO helper

Guards (manter):

  • RecommendationsId == null → return sem publicar
  • cart == null || cart.Id == 0AddError (corrigir typo "carinho" → "carrinho")

Remover: acesso direto ao message bus.

4.4.4 CartService — add/remove

Arquivo: Application/CartService.cs

Remover:

  • Campo _cartRecommendationHistoryRepository
  • Parâmetro construtor ICartRecommendationHistoryRepository
  • LastForCartId + Guid.NewGuid() fallback
  • Funções locais WithError / WithSucesso (4 blocos)

Adicionar:

  • Campo _recommendationHistoryPublisher
  • Método privado auxiliar (opcional, reduz duplicação):
private void TryPublishAddItem(CartModel cart, AddItemToCartCommand cmd, object response, int statusCode)
{
if (cart.RecommendationsId is not Guid id) return;
_ = _publisher.PublishAddItemAsync(_userProvider.GetSchemaName(), id, cart, cmd, response, statusCode);
}

Regras de publicação:

CenárioPublica?StatusCode
cart.RecommendationsId == nullNão
Carrinho não encontrado (add)Não
Carrinho não encontrado (remove)Não
SaleEcommerceSim (erro negócio)400
Validações de recomendaçãoSim400
Add/remove sucessoSim200
Remove item não encontradoNão— (idempotência sem side-effect)
Recomendação já adicionada (add no-op)Sim (200)Comportamento atual — documentar

Decisão em aberto: remove item não encontrado. Proposta: não publicar (sem mudança de estado). Se produto quiser auditoria de tentativas, publicar 200.

Null-safety:

cart.Store?.CodeStore ?? string.Empty // no publisher, não no CartService

4.4.5 DependencyInjectionConfig

services.AddScoped<ICartRecommendationHistoryPublisher, CartRecommendationHistoryPublisher>();

Manter registros existentes de listener, repository, decorator.

4.4.6 O que NÃO alterar

ComponenteMotivo
CartRecommendationHistoryListenerServiceConsumer funcional
CartRecommendationHistoryServicePersistência funcional
CartRecommendationsEvent factoriesJá corretas (nits opcionais)
MessageBusServiceCachedDecoratorCache performático — hot path

5. Comparativo antes/depois

5.1 Performance

OperaçãoAntesDepois
Create cart0 queries extras para histórico0 (inalterado)
Add item1x LastForCartId + fire-and-forget0 queries extras — lê cart.RecommendationsId já no entity
Remove item1x LastForCartId + fire-and-forget0 queries extras
URL fila SQSCache 3h (decorator)Mantido

5.2 Manutenibilidade

AntesDepois
Lógica SQS em 3 lugares1 publisher
4 blocos WithError/WithSucesso2 helpers ou chamadas diretas ao publisher
Correlação frágil (Guid.NewGuid)ID persistido no create
Falha silenciosaLogError estruturado

5.3 Clareza

AntesDepois
SendEvent genéricoPublishAddItemAsync / PublishRemoveItemAsync
Serializa CartModel inteiroDTO enxuto
Mistura negócio + observabilidade em local functionsSeparação por serviço

6. Plano de testes (xunit-tests)

6.1 Novos testes

ArquivoCenários
Cart.UnitTests/Infra/Services/CartRecommendationHistoryPublisherTest.csSucesso publica FIFO; messageGroupId = recommendationsId; exceção logada sem propagar; queue URL vazio logado
CommandToCartModelHandlerTest.cs (novo ou estender)command.RecommendationsIdcart.RecommendationsId setado

6.2 Atualizar existentes

ArquivoMudanças
PublishRecommendationHistoryHandlerTest.csMock ICartRecommendationHistoryPublisher em vez de message bus direto; corrigir constante "carrinho"
AddItemToCartHandlerTests.csRemover mock LastForCartId; mock publisher; Verify publish em sucesso/erro; Never quando RecommendationsId == null
RemoveCartItemHandlerTests.csIdem; assert Never em item não encontrado
LoadStoreModelHandlerTest.csCorrigir teste RepositoryThrows — handler precisa try/catch OU teste removido

6.3 Convenções xunit obrigatórias

  • [Trait("Layer", "Application - Commands")] em cada [Fact] (não só na classe)
  • DisplayName em português
  • AAA com comentários
  • SUT no construtor quando setup é idêntico

7. Sequência de implementação (quando aprovado)

Fase 1 — DbCore
└─ CartModel.RecommendationsId + migration

Fase 2 — Publisher
└─ Interface + implementação + testes unitários do publisher

Fase 3 — Create flow
└─ Popular RecommendationsId nos handlers
└─ Edge case TransactionId
└─ Refatorar PublishRecommendationHistoryHandler
└─ Atualizar testes create

Fase 4 — Add/Remove
└─ Refatorar CartService
└─ Remover LastForCartId
└─ Atualizar testes add/remove

Fase 5 — Limpeza
└─ Remover LastForCartId do repository (se órfão)
└─ Nits: usings, TResq→TReq, typo
└─ Atualizar review-api.md

Estimativa de arquivos tocados: ~15 alterados, ~3 novos, 1 migration.


8. Riscos e mitigações

RiscoMitigação
Carts criados antes da migration sem RecommendationsIdAdd/remove não publicam histórico (null check) — aceitável
Idempotência create com TransactionId não persiste RecommendationsIdEdge case documentado na fase 3
Fire-and-forget perde evento se processo morre antes do Task.RunMesmo risco do AllLogs/Checkout — fora de escopo (sem DLQ por requisito)
Submodule db-core precisa releaseCoordenar versão ZZApp.Core.OrgDB no Cart.Domain

9. Fora de escopo (deliberado)

  • Domain events via BaseEntity
  • DLQ / retry policy na fila
  • Endpoints de consulta do histórico
  • Refatorar add/remove para command handlers dedicados (melhoria futura)
  • GetTopicArn guard no decorator (não é hot path FIFO)
  • Remover referência dupla ZZApp.MessageBus + submodule (issue separada)

10. Documentação de pesquisa

ArquivoConteúdo
context/domain-events-analysis.mdPor que não usar domain events
context/event-handlers-pattern.mdPadrão Event Handler Coezzion
context/log-queue-reference.mdReferência AllLogs / LogQueueService
context/estado-atual-implementacao.mdSnapshot do código atual e problemas
review-api.mdCode review da implementação atual

11. Checklist de aprovação

Antes de implementar, validar:

  • Concordância com persistir RecommendationsId no CartModel (migration db-core)
  • Concordância com remover LastForCartId do hot path
  • Comportamento em remove item não encontrado: publicar ou não?
  • Edge case TransactionId + RecommendationsId: abordagem no SaveCartHandler
  • Nome final: ICartRecommendationHistoryPublisher vs ICartRecommendationHistoryQueueService
  • Versão do pacote ZZApp.Core.OrgDB após migration