Skip to main content

Code Review — Task 09 / 193223 (Upsell Recommendation)

Data: 2026-06-08
Commit base: 8daa50cfeat(#194760): implementa tela e lógica de recomendação upsell
Escopo: último commit + alterações não commitadas (working tree)


Escopo do review

Commit 8daa50c — 24 arquivos, 3.018 inserções / 33 remoções

CategoriaArquivos
Controllercart_controller.dart, upsell_recommendation_controller.dart
Modelcart_dto.dart, upsell_recommendation_item.dart (novo)
Providerupsell_recommendation_provider.dart (novo)
Tela / Widgetsupsell_recommendation_screen.dart, upsell_bottom_bar.dart, upsell_recommendation_card.dart, upsell_size_selection_modal.dart (novos)
Navegaçãoupsell_navigation_helper.dart (novo), app_routes.dart, app_pages.dart
Utilitáriosapi_utils.dart, pages_titles.dart
Themezz_error_state.dart (novo), coezzion_design_flutter.dart
Testesupsell_recommendation_controller_test.dart, cart_dto_test.dart, cart_controller_add_recommended_products_test.dart (novo), upsell_recommendation_item_test.dart (novo)

Alterações não commitadas — 7 arquivos modificados + 1 arquivo novo

ArquivoTipoMudança
upsell_recommendation_controller.dartModifiedRefatoração de formatação + alinhamento de indentação
cart_dto.dartModifiedRenomeia chave recommendedItemscartItemRecommendations
upsell_recommendation_screen.dartModifiedTroca ZzEmptyState por ZzEmptyStateNew.withIcon
upsell_bottom_bar.dartModifiedRefatoração: enum UpsellBottomBarState + 4 widgets privados + shimmer
zz_empty_state_new.dartModifiedAdiciona factory withIcon + migra de flutter_phosphor_icons para phosphor_flutter
pubspec.yamlModifiedAdiciona dependência phosphor_flutter: ^2.1.0
test/models/cart/cart_dto_test.dartModifiedAtualiza chaves para cartItemRecommendations
test/.../upsell_bottom_bar_test.dartUntrackedNovo: testes unitários para UpsellBottomBar.resolveState

Conformidade com os requisitos

RequisitoStatusObservação
RF-01 — Chamada à API na entradaOKneedFetchRecommendations + addPostFrameCallback + cache via hasLoadedRecommendations
RF-02 — Exibição de cards / vazio / erroOKUpsellRecommendationCard, ZzErrorState, ZzEmptyStateNew.withIcon
RF-03 — Card não selecionadoOKbuildSizeChips() + trailing plus_circle
RF-04 — Card selecionadoOKchip único + trailing check_circle_fill + desselecionar
RF-05 — Modal de seleção de tamanhoOKUpsellSizeSelectionModal com GetxBottomSheet
RF-06 — SkeletonOK_buildSkeleton() na tela + _UpsellBottomBarSkeleton na bottom bar (alteração não commitada)
RF-07 — Botão ADICIONAROK_onAddRecommendation com guard de seleção + modal de alerta
RF-08 — Botão NÃO QUEROOK_onSkipRecommendation + skipAllRecommendations()
RF-09 — Model UpsellRecommendationItemOKid, sku, name, image, price, fullPrice, sizesStockStore, hasStock, selectedSize, isSelected, hasMarkdown, buildSizeChips, toProduct
RF-10 — UpsellRecommendationCard stand-aloneOKLayout Row + trailing interativo + corpo sem ação
RF-11 — Extensão do controllerOKrecommendedItems, isLoading, hasError, hasLoadedRecommendations, fetchRecommendations, retry, toggleItemSelection, skipAllRecommendations, hasAnySelected, auto-reset
RF-12 — Estrutura da telaOKStatelessWidget + AppGetBuilder + ZzAppBar + NewSellHomeButton + bottomNavigationBar
RF-13 — recommendedItems no CartDTOOK*Campo presente; chave renomeada para cartItemRecommendations nas alterações não commitadas
RF-14 — CartController.addRecommendedProductsOKFiltro isSelected, conversão, changeWith + addRecommendedItems
RF-15 — Preservação de estadoOKlazySingleton + _resetState no null do activeCartDTO

* Ver item 1 dos pontos de atenção (ausência em toJsonPut).


Pontos fortes

  1. Abordagem testável com funções injetáveis — O UpsellRecommendationController expõe @visibleForTesting para providerPost, getActiveCartDTO, getActiveStore, getRecommendationUrl, permitindo substituição total das dependências sem DI real nos testes.

  2. Cobertura de testes abrangente — 439 linhas de testes no controller test, 333 linhas no cart_dto_test, testes dedicados para addRecommendedProducts e UpsellRecommendationItem. Casos de borda cobertos: cache-hit, erro de rede, activeStore null, CPF vazio, reset de estado.

  3. Separação de responsabilidadesUpsellRecommendationItem é um model puro com lógica de UI (buildSizeChips, toProduct) sem dependências externas. UpsellNavigationHelper centraliza a lógica condicional de navegação, evitando duplicação.

  4. Componentização da UI — Tela quebrada em UpsellRecommendationCard, UpsellSizeSelectionModal, UpsellBottomBar. Cada widget focado e reutilizável.

  5. ZzErrorState genérico — Componente de erro reutilizável com title, description, image e retry, seguindo o design system do app.

  6. Refatoração de UpsellBottomBar (alteração não commitada) — A mudança para enum UpsellBottomBarState + resolveState() estática + 4 widgets privados (_UpsellBottomBarSkeleton, _UpsellBottomBarError, _UpsellBottomBarEmpty, _UpsellBottomBarWithItems) é uma melhoria expressiva. Transforma um build() com lógica imperativa em um switch declarativo fácil de ler e testar. A função pura resolveState com testes unitários dedicados (upsell_bottom_bar_test.dart) cobre todas as combinações de flags.

  7. Renomeio de chave recommendedItemscartItemRecommendations (alteração não commitada) — Alinhamento com o contrato esperado pelo backend. Atualizado consistentemente em fromJson, toJson, toJsonLocal e nos testes.

  8. Migração flutter_phosphor_iconsphosphor_flutter (alteração não commitada)phosphor_flutter é a lib oficial e mantida. A migração com hide PhosphorIcons nos imports de transição evita colisão de símbolos.


Pontos de atenção / melhorias sugeridas

1. recommendedItems em toJson() mas ausente em toJsonPut()

Arquivo: lib/models/cart/cart_dto.dart:285,318

O campo cartItemRecommendations está presente em toJson() e toJsonLocal(), mas ausente em toJsonPut(). Se toJsonPut() for usado para enviar o cart ao backend, os itens recomendados serão perdidos na chamada PUT. Se são apenas estado local de UI, deveriam ser removidos de toJson() também ou deveria haver comentário justificando a diferença.

Recomendação: Verificar contrato com backend. Se recomendados devem ser enviados, adicionar a toJsonPut(). Se não, remover de toJson().

2. navigateAfterCustomerStep (Get.offNamed) — código morto

Arquivo: lib/shared/utils/upsell_navigation_helper.dart:18-23

O método navigateAfterCustomerStep() (que usa Get.offNamed, substituindo a rota) foi declarado mas não é chamado em lugar nenhum do código. Todas as chamadas existentes usam navigateToAfterCustomerStep() (com Get.toNamed, push).

Recomendação: Remover ou documentar quando será usado.

3. _onCartChanged: evaluateEligibility chamado antes do early-return

Arquivo: lib/controllers/sell/new_sell/upsell_recommendation_controller.dart:106-111

void _onCartChanged(CartDTO? cart) {
final newValue = evaluateEligibility(cart); // cart pode ser null
if (cart == null) {
_resetState();
return;
}
...
}

evaluateEligibility(null) retorna false — correto. Mas há redundância: _resetState() já seta reativos e o código seguinte faria _isActive.value = false novamente com o newValue. Funcionalmente inofensivo, mas desnecessário.

Recomendação: Mover o early-return para antes:

if (cart == null) {
_resetState();
return;
}
final newValue = evaluateEligibility(cart);

4. _resetState não cancela fetch em andamento

Arquivo: lib/controllers/sell/new_sell/upsell_recommendation_controller.dart:215-221

Se fetchRecommendations estiver em andamento (await no providerPost) e _resetState for chamado (ex: activeCartDTO emite null porque o usuário cancelou a venda), hasLoadedRecommendations e hasError são resetados, mas a resposta pendente ainda será processada quando chegar, populando recommendedItems após o reset.

Recomendação: Adicionar token de cancelamento (ex: incrementar _fetchGeneration e verificar antes de assignAll).

5. UpsellRecommendationProvider com URL vazia

Arquivo: lib/providers/product_recommendation/upsell_recommendation_provider.dart:3-4

class UpsellRecommendationProvider extends DefaultDioProvider {
UpsellRecommendationProvider() : super(url: '');
}

A URL é sempre passada via customUrl no método post(). Funciona, mas é frágil: se alguém chamar provider.post() sem customUrl, a requisição vai para URL vazia sem erro claro.

Recomendação: Sobrescrever post para exigir customUrl ou lançar erro explícito.

6. toggleItemSelection: mutabilidade compartilhada no model

Arquivo: lib/controllers/sell/new_sell/upsell_recommendation_controller.dart:201-203

void toggleItemSelection(UpsellRecommendationItem item, String? size) {
item.selectedSize = size;
update();
}

selectedSize é um campo mutável no model. A tela passa referência do item que está no recommendedItems (RxList). Como é a mesma instância, funciona — mas é um antipadrão em arquiteturas reativas: qualquer referência ao item pode mutar selectedSize sem notificar o controller.

Impacto real: Baixo no escopo atual. Mas se o model for compartilhado entre controllers no futuro, vira bug.

Recomendação: Considerar tornar UpsellRecommendationItem imutável e gerenciar selectedSize num Map interno do controller.

7. Nomenclatura divergente: addRecommendedProducts vs addRecommendedItems

Arquivos: lib/models/cart/cart_dto.dart (extension addRecommendedItems), lib/controllers/cart/cart_controller.dart (método addRecommendedProducts)

O CartDTO extension usa addRecommendedItems(List<CartItemDTO>), o CartController expõe addRecommendedProducts(List<UpsellRecommendationItem>). Os nomes são diferentes ("Items" vs "Products") e fazem sentido em cada contexto, mas podem causar confusão.

Recomendação: Documentar a diferença ou unificar nomenclatura.

8. Transição incompleta para phosphor_flutter

Arquivos: upsell_recommendation_screen.dart (importa ambas as libs), upsell_bottom_bar.dart (ainda usa PhosphorIcons.caret_right da lib antiga)

A migração para phosphor_flutter está em andamento. A tela importa ambas as libs com hide PhosphorIcons, mas a upsell_bottom_bar.dart ainda referencia PhosphorIcons.caret_right da lib antiga (flutter_phosphor_icons). O zz_empty_state_new.dart já migrou para PhosphorIconsRegular.*.

Recomendação: Concluir a migração em todos os arquivos do escopo, substituindo PhosphorIcons.* por PhosphorIconsRegular.* ou PhosphorIconsFill.* conforme apropriado, e removendo a dependência antiga do pubspec.yaml.


Checklist

  • Confirmar se recommendedItems deve estar em toJsonPut()
  • Remover navigateAfterCustomerStep não utilizado (ou documentar uso futuro)
  • Reordenar _onCartChanged — early-return antes de evaluateEligibility
  • Adicionar token de cancelamento de fetch pendente no _resetState
  • Concluir migração PhosphorIconsPhosphorIconsRegular/PhosphorIconsFill
  • Avaliar tornar selectedSize imutável no model
  • Documentar contrato da API com backend para cartItemRecommendations

Verdict

Aprovado com ressalvas. O commit base (8daa50c) está arquiteturalmente sólido, com testes abrangentes e boa componentização. As alterações não commitadas melhoram significativamente a qualidade: a refatoração da UpsellBottomBar para enum + resolveState() pura é o destaque positivo.

Pontos que merecem atenção antes do merge:

  • #1 — Ausência de cartItemRecommendations em toJsonPut() deve ser resolvida com confirmação do contrato backend
  • #4 — Race condition no _resetState durante fetch pendente
  • #8 — Migração incompleta de flutter_phosphor_iconsphosphor_flutter

Os demais pontos são melhorias de qualidade sem bloqueio.