Code Review — Task 09 / 193223 (Upsell Recommendation)
Data: 2026-06-08
Commit base: 8daa50c — feat(#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
| Categoria | Arquivos |
|---|---|
| Controller | cart_controller.dart, upsell_recommendation_controller.dart |
| Model | cart_dto.dart, upsell_recommendation_item.dart (novo) |
| Provider | upsell_recommendation_provider.dart (novo) |
| Tela / Widgets | upsell_recommendation_screen.dart, upsell_bottom_bar.dart, upsell_recommendation_card.dart, upsell_size_selection_modal.dart (novos) |
| Navegação | upsell_navigation_helper.dart (novo), app_routes.dart, app_pages.dart |
| Utilitários | api_utils.dart, pages_titles.dart |
| Theme | zz_error_state.dart (novo), coezzion_design_flutter.dart |
| Testes | upsell_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
| Arquivo | Tipo | Mudança |
|---|---|---|
upsell_recommendation_controller.dart | Modified | Refatoração de formatação + alinhamento de indentação |
cart_dto.dart | Modified | Renomeia chave recommendedItems → cartItemRecommendations |
upsell_recommendation_screen.dart | Modified | Troca ZzEmptyState por ZzEmptyStateNew.withIcon |
upsell_bottom_bar.dart | Modified | Refatoração: enum UpsellBottomBarState + 4 widgets privados + shimmer |
zz_empty_state_new.dart | Modified | Adiciona factory withIcon + migra de flutter_phosphor_icons para phosphor_flutter |
pubspec.yaml | Modified | Adiciona dependência phosphor_flutter: ^2.1.0 |
test/models/cart/cart_dto_test.dart | Modified | Atualiza chaves para cartItemRecommendations |
test/.../upsell_bottom_bar_test.dart | Untracked | Novo: testes unitários para UpsellBottomBar.resolveState |
Conformidade com os requisitos
| Requisito | Status | Observação |
|---|---|---|
| RF-01 — Chamada à API na entrada | OK | needFetchRecommendations + addPostFrameCallback + cache via hasLoadedRecommendations |
| RF-02 — Exibição de cards / vazio / erro | OK | UpsellRecommendationCard, ZzErrorState, ZzEmptyStateNew.withIcon |
| RF-03 — Card não selecionado | OK | buildSizeChips() + trailing plus_circle |
| RF-04 — Card selecionado | OK | chip único + trailing check_circle_fill + desselecionar |
| RF-05 — Modal de seleção de tamanho | OK | UpsellSizeSelectionModal com GetxBottomSheet |
| RF-06 — Skeleton | OK | _buildSkeleton() na tela + _UpsellBottomBarSkeleton na bottom bar (alteração não commitada) |
| RF-07 — Botão ADICIONAR | OK | _onAddRecommendation com guard de seleção + modal de alerta |
| RF-08 — Botão NÃO QUERO | OK | _onSkipRecommendation + skipAllRecommendations() |
| RF-09 — Model UpsellRecommendationItem | OK | id, sku, name, image, price, fullPrice, sizesStockStore, hasStock, selectedSize, isSelected, hasMarkdown, buildSizeChips, toProduct |
| RF-10 — UpsellRecommendationCard stand-alone | OK | Layout Row + trailing interativo + corpo sem ação |
| RF-11 — Extensão do controller | OK | recommendedItems, isLoading, hasError, hasLoadedRecommendations, fetchRecommendations, retry, toggleItemSelection, skipAllRecommendations, hasAnySelected, auto-reset |
| RF-12 — Estrutura da tela | OK | StatelessWidget + AppGetBuilder + ZzAppBar + NewSellHomeButton + bottomNavigationBar |
RF-13 — recommendedItems no CartDTO | OK* | Campo presente; chave renomeada para cartItemRecommendations nas alterações não commitadas |
RF-14 — CartController.addRecommendedProducts | OK | Filtro isSelected, conversão, changeWith + addRecommendedItems |
| RF-15 — Preservação de estado | OK | lazySingleton + _resetState no null do activeCartDTO |
*Ver item 1 dos pontos de atenção (ausência emtoJsonPut).
Pontos fortes
-
Abordagem testável com funções injetáveis — O
UpsellRecommendationControllerexpõe@visibleForTestingparaproviderPost,getActiveCartDTO,getActiveStore,getRecommendationUrl, permitindo substituição total das dependências sem DI real nos testes. -
Cobertura de testes abrangente — 439 linhas de testes no controller test, 333 linhas no cart_dto_test, testes dedicados para
addRecommendedProductseUpsellRecommendationItem. Casos de borda cobertos: cache-hit, erro de rede, activeStore null, CPF vazio, reset de estado. -
Separação de responsabilidades —
UpsellRecommendationItemé um model puro com lógica de UI (buildSizeChips,toProduct) sem dependências externas.UpsellNavigationHelpercentraliza a lógica condicional de navegação, evitando duplicação. -
Componentização da UI — Tela quebrada em
UpsellRecommendationCard,UpsellSizeSelectionModal,UpsellBottomBar. Cada widget focado e reutilizável. -
ZzErrorStategenérico — Componente de erro reutilizável com title, description, image e retry, seguindo o design system do app. -
Refatoração de
UpsellBottomBar(alteração não commitada) — A mudança para enumUpsellBottomBarState+resolveState()estática + 4 widgets privados (_UpsellBottomBarSkeleton,_UpsellBottomBarError,_UpsellBottomBarEmpty,_UpsellBottomBarWithItems) é uma melhoria expressiva. Transforma umbuild()com lógica imperativa em um switch declarativo fácil de ler e testar. A função puraresolveStatecom testes unitários dedicados (upsell_bottom_bar_test.dart) cobre todas as combinações de flags. -
Renomeio de chave
recommendedItems→cartItemRecommendations(alteração não commitada) — Alinhamento com o contrato esperado pelo backend. Atualizado consistentemente emfromJson,toJson,toJsonLocale nos testes. -
Migração
flutter_phosphor_icons→phosphor_flutter(alteração não commitada) —phosphor_flutteré a lib oficial e mantida. A migração comhide PhosphorIconsnos 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
recommendedItemsdeve estar emtoJsonPut() - Remover
navigateAfterCustomerStepnão utilizado (ou documentar uso futuro) - Reordenar
_onCartChanged— early-return antes deevaluateEligibility - Adicionar token de cancelamento de fetch pendente no
_resetState - Concluir migração
PhosphorIcons→PhosphorIconsRegular/PhosphorIconsFill - Avaliar tornar
selectedSizeimutá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
cartItemRecommendationsemtoJsonPut()deve ser resolvida com confirmação do contrato backend - #4 — Race condition no
_resetStatedurante fetch pendente - #8 — Migração incompleta de
flutter_phosphor_icons→phosphor_flutter
Os demais pontos são melhorias de qualidade sem bloqueio.