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 viaactiveCartDTO.trigger()noOpenSalesController. saleEcommerce(bool?): tri-state —null= não definido,true= ecommerce,false= estoque loja. Lock-in no primeiro produto adicionado via??; reseta paranullquando carrinho esvazia.addItem()(extensionMutableCartDTO): três branches —product:(normal),cartItemDTO:(item pré-construído),listCartItemDTO:(batch).copyWith()usa padrãoValue()/Nil()para distinguir "não informado" de "setar null".versionincrementa em todocopyWith().- 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. Usaproduct.fullPricecomounitPrice(PVL), calculadiscountMarkdownquandohasMarkdown. - 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 (comquantity: 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 paraaddRecommendedProducts()(nova task).changeWithAsync()(assíncrono): para mutações que envolvem chamada de API.setProducts(List<Product>)(line 709): substitui todos oscartItems, preservandoCartItemDTOexistentes porcartItemId.cartDTOgetter (line 142): sempre retorna uma cópia (activeCartDTO.value?.copyWith()), nunca a referência original.- Método a ser criado:
addRecommendedProducts(List<UpsellRecommendationItem>)— filtraisSelected, converte paraCartItemDTO, salva emcartDTO.recommendedItemsviachangeWith().
OpenSalesController
Arquivo: coezzion_vendas_app/lib/controllers/open_sales/open_sales_controller.dart
activeCartDTO(Rxn<CartDTO?>): observable do cart ativo. Ao mudar, sincroniza comPrefsService.createNewCartDTO(): cria cart vazio comcartItems: [],saleEcommerce: null.clearActiveCart(): setaactiveCartDTO.value = null→ dispara auto-reset doUpsellRecommendationController(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
lazySingletonemget_it.dart(task-08): mesma instância sobrevive entre telas do fluxo de venda. - Subscreve
activeCartDTO: quandonull→reset()(limpa cache,hasLoadedRecommendations = false). - Cache:
RxList<UpsellRecommendationItem> recommendedItems+RxBool hasLoadedRecommendations. Ao entrar na tela, sehasLoadedRecommendations == true, exibe cache sem re-fetch. fetchRecommendations(): só dispara API sehasLoadedRecommendations == false.
Decisões tomadas
| # | Questão | Decisão |
|---|---|---|
| Q1 | Itens recomendados são CartItems ou lista separada? | Lista separada (CartDTO.recommendedItems: List<CartItemDTO>). Não compõem itemsTotal/total. |
| Q2 | Como adicionar recommendedItems ao CartDTO? | Novo método CartController.addRecommendedProducts() usando changeWith() síncrono, sem chamada de API. |
| Q3 | Model 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. |
| Q4 | Como preservar estado ao retornar para a tela? | UpsellRecommendationController (lazySingleton) mantém cache. hasLoadedRecommendations controla se re-fetch é necessário. |
| Q5 | Quando resetar o cache? | Quando activeCartDTO → null (venda finalizada/nova venda). |
| Q6 | recommendedItems usa copyWith() com que padrão? | Value([]) para lista vazia, const Nil() para null. Segue o padrão existente do CartDTO. |