feat(products): catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade com submenu e dropdowns
Menu Produtos ganha submenu (AppShell agora suporta itens aninhados) para cadastrar Tipos/Grupos/Marcas/Unidades; Subgrupo é gerenciado a partir da tela do Grupo (sempre pertence a um). O cadastro de item passa a usar dropdowns desses catálogos em vez de texto livre — exatamente o que o comentário original da migration de products já previa como trabalho futuro. Backend: 5 tabelas de catálogo simples (mesmo padrão dos catálogos fiscais), products ganha as FKs correspondentes com backfill, e endpoints CRUD (sem exclusão física, active=false é o "apagar"). Corrige também um buraco de integridade real encontrado na integração: PATCH /v1/products/:id usava COALESCE para os campos de catálogo opcionais, então não dava para distinguir "não mexer" de "limpar" — trocar o Grupo de um produto sem tocar no Subgrupo deixava o vínculo antigo órfão e inconsistente, sem nenhuma validação pegando isso. Agora a atualização valida o estado efetivo resultante (bloqueia com 400 em vez de corromper o vínculo) e productSubgroupId: null limpa de verdade. Suíte: 95/95 verdes (92→95). pnpm turbo run typecheck build: 28/28. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -617,3 +617,32 @@ Cobertos em `test/signature-envelope-flow.spec.ts` (72/72 antes da leva de rate
|
||||
|
||||
### Pendências
|
||||
`DECLINED` (recusa de assinatura) segue sem rota que o produza — reservado no schema, mesma lacuna do legado. `contract_versions`/`contract_amendments` (ADR-0006, adiados desde a leva de trava de edição de contrato) agora estão desbloqueados — o fluxo de assinatura já existe — mas ainda não iniciados. `reseller-web`/`subscriber-web` seguem só com o shell de login — nenhuma tela própria, incluindo nada do módulo de assinatura (fora de escopo desta leva).
|
||||
|
||||
---
|
||||
|
||||
## Catálogos de Produto (Tipo/Grupo/Subgrupo/Marca/Unidade) — submenu, telas e dropdowns no cadastro de item
|
||||
|
||||
Data: 2026-09-17. Usuário pediu: "o menu produtos precisa ter sub menus, aonde cadastraremos os grupos subgrupos marcas e tipo e unidades e dentro do cadastro do item ai essas informações sera apenas dropdown dos cadastros feitos anteriormente." O próprio cabeçalho da migration original de `products` (Fase 2) já previa isso como trabalho futuro ("`product_type` is free text for now... tightening it to a CHECK/lookup table is future work"). Confirmado antes de começar: a tabela `products` só tinha 37 linhas, todas de teste/QA — backfill trivial, sem dado histórico real em risco.
|
||||
|
||||
### Backend (agente `eden-commercial`)
|
||||
|
||||
Migration `packages/database/migrations/1789694035000_product-catalogs.cjs`: 5 tabelas novas no formato "catálogo simples" (mesmo shape de `fiscal_ncm`/`fiscal_cfop`, sem os campos de sincronização externa — são catálogos 100% manuais) — `product_types`, `product_groups`, `product_subgroups` (FK `group_id → product_groups`, `ON DELETE RESTRICT`), `brands`, `units` (seedada com `'Unidade'`). `products` ganha `product_type_id`/`product_group_id`/`product_subgroup_id`/`brand_id`/`unit_id`, backfill a partir das colunas TEXT antigas (`product_type`/`unit` sempre presentes viram `NOT NULL`), colunas antigas dropadas. `down` reversível de verdade, testado (`migrate` → `migrate:down` → `migrate` de novo).
|
||||
|
||||
Endpoints novos (mesmo controller de `products`, padrão de agrupamento do `FiscalController`): `GET/POST /v1/products/{types,groups,brands,units}`, `PATCH .../:id`, `GET/POST /v1/products/subgroups` (`groupId` filtra a listagem, obrigatório no create), `PATCH /v1/products/subgroups/:id`. Leitura aberta a qualquer autenticado, escrita exige `products:edit` — mesma regra já documentada no `ProductsController`. Sem rota de exclusão física em nenhum dos 5 (mesmo padrão dos catálogos fiscais) — `active=false` é o "apagar". `GET /v1/products`/`GET /v1/products/:id` agora fazem JOIN e devolvem `*_name` resolvido ao lado de cada `*_id`. `CreateProductDto`/`UpdateProductDto` trocaram os campos de texto livre por `productTypeId`/`productGroupId`/`productSubgroupId`/`brandId`/`unitId` (UUID), com validação de existência e de consistência subgrupo↔grupo.
|
||||
|
||||
**Achado real corrigido diretamente por mim (não delegado)**: o agente de frontend, ao integrar, encontrou e reportou com precisão um buraco de integridade de dados que o `eden-commercial` tinha deixado passar — `PATCH /v1/products/:id` usa `COALESCE` para os campos opcionais, então não existia forma de expressar "limpar este campo" distinta de "não mexer nele" (ambos colapsavam no mesmo parâmetro `NULL` do SQL). Consequência real: editar um produto trocando o Grupo sem tocar no Subgrupo deixava o `product_subgroup_id` antigo órfão, agora inconsistente com o novo grupo, sem nenhuma validação pegando isso (a checagem de consistência só rodava quando `productSubgroupId` vinha no mesmo PATCH). Corrigido em `apps/api/src/products/products.service.ts`/`packages/database/src/repositories/products.ts`: `productSubgroupId: null` agora é uma instrução explícita de "remover" (nunca tenta buscar um subgrupo com id `null`), a validação de update passou a checar o estado **efetivo resultante** (valor do DTO onde informado, senão o valor atual do produto) — então trocar de grupo sem resolver o subgrupo agora é bloqueado com 400 claro em vez de corromper o vínculo silenciosamente — e uma função de repositório dedicada (`clearProductSubgroup`) contorna a limitação do `COALESCE` para esse campo especificamente. Frontend (`ProductFormPage.tsx`) ajustado para mandar `null` explícito (não `undefined`) quando o campo é limpo, e o aviso que o agente tinha colocado como paliativo foi removido — o problema está resolvido de verdade, não só sinalizado. Não estendi o mesmo tratamento a `productGroupId`/`brandId` (limitação de "não dá para limpar de volta pra vazio via PATCH" pré-existente e mais ampla no código, sem risco de invariante quebrada — fora de escopo).
|
||||
|
||||
3 testes novos em `apps/api/test/product-catalogs.spec.ts` cobrindo exatamente esse ciclo (bloqueio, limpeza explícita, troca de grupo bem-sucedida depois). Suíte completa: 95/95 verdes (92→95, incluindo os 15 do agente + os 3 meus).
|
||||
|
||||
### Frontend (agente `eden-frontend`)
|
||||
|
||||
`packages/ui/src/components/AppShell.tsx`: `AppShellNavItem` ganhou `children?` — item com filhos vira accordion (chevron, auto-expande quando a rota atual é de um filho), sem dependência nova. Verificado sem regressão nos 3 apps (`core-web`/`reseller-web`/`subscriber-web` usam o mesmo componente).
|
||||
|
||||
Menu "Produtos" ganhou submenu: Itens/Tipos/Grupos/Marcas/Unidades (Subgrupo deliberadamente sem entrada própria — sempre gerenciado a partir da tela de um Grupo específico, via painel "Ver subgrupos", já que subgrupo não existe fora do contexto de um grupo). 4 telas novas em `apps/core-web/src/routes/products/catalogs/` reaproveitando o padrão já usado em `fiscal/NcmListPage.tsx` (lista + formulário inline de criação + toggle ativo/inativo). `ProductFormPage.tsx`: Tipo/Unidade/Grupo/Subgrupo/Marca viraram dropdowns (Subgrupo desabilitado até escolher um Grupo, refaz fetch por `groupId` a cada troca, limpa a seleção se deixar de pertencer ao grupo novo — e agora manda `null` explícito nesse caso, fechando o ciclo com a correção de backend acima).
|
||||
|
||||
### Verificação
|
||||
|
||||
`pnpm turbo run typecheck build` 28/28 (verificado de forma independente por mim antes e depois da correção do bug de COALESCE). Suíte de testes 95/95. Testado ao vivo via `curl` contra `eden-api`/`eden-core` reais, através do proxy same-origin: criação de tipo/grupo/subgrupo/marca/unidade, criação de produto referenciando os 5, o 400 exato de "grupo incompatível" ao trocar de grupo sem resolver o subgrupo, `productSubgroupId: null` limpando de verdade (confirmado por re-GET), e a troca de grupo funcionando depois de limpo. Rotas `/products/types`, `/products/groups`, `/products/brands`, `/products/units` respondendo 200 pelo proxy (porta 3001). Linhas de teste (`qa-test-*`/`smoke-test-*`) desativadas nos 5 catálogos e no produto de teste ao final — estado ativo ficou limpo (`service`/`telecom` em Tipos, `Unidade` em Unidades, Grupos/Subgrupos/Marcas vazios, prontos para o usuário cadastrar os seus).
|
||||
|
||||
### Pendências
|
||||
Suítes de teste futuras vão continuar acrescentando linhas `qa-test-*` permanentes nesses 5 catálogos (sem rota de exclusão física, mesmo padrão aceito para os catálogos fiscais) — precisa de desativação manual periódica até (se algum dia for justificado) existir uma rota de exclusão física ou um processo de limpeza dedicado. "Não dá para limpar Grupo/Marca de volta para vazio via PATCH" continua uma limitação pré-existente e mais ampla do padrão `COALESCE` usado em `updateProduct` — não corrigida (sem risco de invariante quebrada, ao contrário do caso do subgrupo).
|
||||
|
||||
Reference in New Issue
Block a user