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:
2026-09-17 23:46:14 -03:00
parent 9c14671e19
commit f38f2407ff
39 changed files with 2738 additions and 137 deletions

View File

@@ -118,3 +118,14 @@ export function qaKey(label: string): string {
// apps/api/src/roles/dto/create-role.dto.ts).
return `qa_${label}_${rand}`.slice(0, 30);
}
/**
* Random-suffixed display name, for fixtures backed by a case-insensitive
* UNIQUE index with no delete endpoint (e.g. product_types/product_groups/
* brands/units) — a hardcoded name would collide with a row left behind by
* a previous run of this suite against the same shared dev database.
*/
export function qaName(label: string): string {
const rand = Math.random().toString(36).slice(2, 8);
return `${QA_PREFIX}${label} ${rand}`;
}