Skip to main content

Pesquisa Técnica — CartDTO e fluxo de adição de produtos no zzapp

Contexto: Task #193223 — Criar tela de recomendação upsell


Investigação da estrutura do CartDTO, CartController, e fluxo de adição de produtos ao carrinho no zzapp, visando entender como integrar os itens recomendados selecionados na tela de upsell.

Código relevante encontrado

CartDTO

Arquivo: coezzion_vendas_app/lib/models/cart/cart_dto.dart

  • Imutável: todas as mutações passam por copyWith() e são persistidas via activeCartDTO.trigger() no OpenSalesController.
  • saleEcommerce (bool?): tri-state — null = não definido, true = ecommerce, false = estoque loja. Lock-in no primeiro produto adicionado via ??; reseta para null quando carrinho esvazia.
  • addItem() (extension MutableCartDTO): três branches — product: (normal), cartItemDTO: (item pré-construído), listCartItemDTO: (batch).
  • copyWith() usa padrão Value()/Nil() para distinguir "não informado" de "setar null". version incrementa em todo copyWith().
  • Campos principais: id, storeId, userId, saleEcommerce, cartItems: List<CartItemDTO>, total, itemsTotal, shipmentValue, customer, dateCreated, version, voucher, showcaseId, attendanceId.
  • Campos NÃO existentes: recommendedItems (a ser adicionado nesta task), isFromRecommendation (a ser adicionado em US separada — CONTEXT.md).

CartItemDTO

Arquivo: coezzion_vendas_app/lib/models/cart/cart_item_dto.dart

  • Factory cartItemDTOByProduct(Product, {size, showcaseId}): converte Product → CartItemDTO. Usa product.fullPrice como unitPrice (PVL), calcula discountMarkdown quando hasMarkdown.
  • Campos: productId, name, sku, image, quantity, size, unitPrice, unitPriceWithDiscount, discount (enum), discountValue, discountMarkdown, brand, hasStock.
  • Reutilizado para recommendedItems — os mesmos campos servem para representar itens recomendados (com quantity: 1, discountType: none, discountValue: 0).

CartController

Arquivo: coezzion_vendas_app/lib/controllers/cart/cart_controller.dart

  • addProduct(Product) (line 524): changeWithAsync()cart.addItem(product:)computeValuesAndTotals()activeCartDTO.trigger().
  • changeWith() (síncrono): para mutações locais sem chamada de API. Usado para addRecommendedProducts() (nova task).
  • changeWithAsync() (assíncrono): para mutações que envolvem chamada de API.
  • setProducts(List<Product>) (line 709): substitui todos os cartItems, preservando CartItemDTO existentes por cartItemId.
  • cartDTO getter (line 142): sempre retorna uma cópia (activeCartDTO.value?.copyWith()), nunca a referência original.
  • Método a ser criado: addRecommendedProducts(List<UpsellRecommendationItem>) — filtra isSelected, converte para CartItemDTO, salva em cartDTO.recommendedItems via changeWith().

OpenSalesController

Arquivo: coezzion_vendas_app/lib/controllers/open_sales/open_sales_controller.dart

  • activeCartDTO (Rxn<CartDTO?>): observable do cart ativo. Ao mudar, sincroniza com PrefsService.
  • createNewCartDTO(): cria cart vazio com cartItems: [], saleEcommerce: null.
  • clearActiveCart(): seta activeCartDTO.value = null → dispara auto-reset do UpsellRecommendationController (ADR-010).

Fluxo completo: Product → CartItemDTO

1. Vendedora seleciona product + size no AddProductBottomSheet
2. product.selectedSize = "39"
3. productController.updateSelectedProducts(product)
→ clona, limpa sizes, mantém só selectedSize com qty 1
4. cartController.addProduct(product)
→ changeWithAsync()
→ cart.addItem(product: product, size: product.selectedSize)
→ saleEcommerce lock-in (??)
→ CartItemDTO.cartItemDTOByProduct(product, size: "39")
→ cartItems.append(cartItemDTO)
→ computeValuesAndTotals()
→ activeCartDTO.trigger(newCart)

Fluxo novo: UpsellRecommendationItem → CartItemDTO (recommendedItems)

1. Vendedora seleciona size via modal na tela de recomendação
2. item.selectedSize = "37"
3. Vendedora clica ADICIONAR RECOMENDAÇÃO
4. UpsellRecommendationController coleta itens com isSelected == true
5. cartController.addRecommendedProducts(selectedItems)
→ changeWith()
→ para cada item: CartItemDTO(productId: id, name, sku, image,
quantity: 1, size: selectedSize, unitPrice: fullPrice ?? price,
unitPriceWithDiscount: price, discountType: none, discountValue: 0)
→ cart.addRecommendedItems(cartItemDTOs)
→ recommendedItems = cartItemDTOs (replace)
→ NÃO chama computeValuesAndTotals() — recommendedItems não compõe total

UpsellRecommendationController (cache por venda)

  • Registrado como lazySingleton em get_it.dart (task-08): mesma instância sobrevive entre telas do fluxo de venda.
  • Subscreve activeCartDTO: quando nullreset() (limpa cache, hasLoadedRecommendations = false).
  • Cache: RxList<UpsellRecommendationItem> recommendedItems + RxBool hasLoadedRecommendations. Ao entrar na tela, se hasLoadedRecommendations == true, exibe cache sem re-fetch.
  • fetchRecommendations(): só dispara API se hasLoadedRecommendations == false.

Decisões tomadas

#QuestãoDecisão
Q1Itens recomendados são CartItems ou lista separada?Lista separada (CartDTO.recommendedItems: List<CartItemDTO>). Não compõem itemsTotal/total.
Q2Como adicionar recommendedItems ao CartDTO?Novo método CartController.addRecommendedProducts() usando changeWith() síncrono, sem chamada de API.
Q3Model para persistência: UpsellRecommendationItem ou CartItemDTO?CartItemDTO — já tem todos os campos necessários (productId, name, sku, image, size, price, fullPrice, discount, brand). UpsellRecommendationItem é model de tela com estado UI (selectedSize); CartItemDTO é model de persistência.
Q4Como preservar estado ao retornar para a tela?UpsellRecommendationController (lazySingleton) mantém cache. hasLoadedRecommendations controla se re-fetch é necessário.
Q5Quando resetar o cache?Quando activeCartDTO → null (venda finalizada/nova venda).
Q6recommendedItems usa copyWith() com que padrão?Value([]) para lista vazia, const Nil() para null. Segue o padrão existente do CartDTO.