Especificação · Catálogo

Cadastro de Item

O registro fundacional do Nexio. Um único agregado que precisa descrever matéria-prima, produto acabado, MRO, serviço, assinatura e ativo — em qualquer setor — com precisão suficiente para cotar, comparar, aprovar e conferir.

substitui o cap. 2 de NEXIO_Catalogo_Itens_v2.0 7 agregados · 1 entidade de configuração alinhada a Centro de Custo, Empresa e Estabelecimento, Local de Entrega, Anexo e GrydFiles · decidida por Fluxo de Aprovação v3.15 · 09/09/2026

Ponto de partida

Decisões tomadas antes desta especificação

Seis escolhas foram fechadas com o time e são premissa de tudo abaixo — a quinta revisada e a sexta acrescentada em 06/09/2026, a segunda alinhada às outras duas árvores do produto em 08/09/2026. Cada uma altera o documento anterior do Catálogo.

Decisão 1 · Tipo

Entidade única com ItemType. Um Item para material, serviço, ativo e assinatura, com campos condicionais validados no domínio. É o que Coupa e Precoro fazem — os dois produtos da amostra que são P2P puro, como o Nexio. O SAP separa material e serviço porque é ERP e precisa de MRP e valoração; nós não.

Decisão 2 · Taxonomia

CategoryNode auto-relacionado. Substitui Category + Subcategory. Profundidade livre no armazenamento, teto por configuração do tenant (maxDepth, default 2). A UI segue com dois selects; o terceiro nível vira um INSERT em vez de migração multi-tenant. Revisada em 08/09/2026: a árvore usa a mecânica do Centro de Custo e do plano de contas — caminho materializado em texto, depth, allowsPosting, vigência opcional, PATCH /parent e PATCH /activate. Três árvores, uma mecânica; o que muda entre elas é o que o nó carrega.

Decisão 3 · Fiscal

Cobertura BR completa, em regime de "esperado". NCM, CEST, GTIN para mercadorias; LC 116, cTribNac e NBS para serviços; destinação predominante para o crédito de IBS/CBS. Nenhum campo fiscal é obrigatório para criar item, e nenhum é fonte de verdade fiscal — são referências de conferência.

Decisão 4 · Estoque

Fora do escopo, com ganchos previstos. O item não carrega saldo, mínimo, ponto de reposição nem localização. O que fica previsto no modelo: controlsStock no tipo, lote e validade como grupo condicional desligado, e a unidade base já sendo a unidade de estoque futura.

Decisão 5 · Nome

A entidade se chama Item, e as linhas de documento se chamam <Documento>Item. Catalog.CatalogItem gagueja — o namespace já diz o módulo. As linhas são RequisitionItem, QuotationItem, PurchaseOrderItem e ReceiptItem: "item" é a palavra que o comprador usa ("o item 3 do pedido"), e o prefixo do documento é o que desfaz a ambiguidade com o mestre. A regra tem dois lados: Item sem prefixo é sempre o mestre do catálogo; com prefixo de documento é sempre a linha que congela o mestre — nunca Line, nunca Item solto num namespace de documento. Ficam fora da regra, por não serem linhas de documento, BudgetLine (fatia do orçamento, camada mestre) e o scope = Line do passo de aprovação (recorte do motor). Revisada em 06/09/2026 — até a v3.7 as linhas se chamavam *Line.

Decisão 6 · Composição

Item não é composto de item. Não existe parentItemId, kit nem lista técnica no mestre — decidido em 06/09/2026. Lista técnica (BOM) é fronteira do ERP; kit de compra é modelo de requisição, o conjunto de linhas pré-preenchidas que a spec da Requisição define; kit que o fornecedor vende como SKU único é item comum; variante é item com atributos; e o código do fornecedor é ItemSupplier. Tudo que o documento congela é do componente — e é por isso que o kit não sobrevive como mestre: não tem NCM, assinatura de atributos, preço nem fornecedor próprios. Ver Fora de escopo.

Fundamentos

Sete princípios que governam o cadastro

1. A descrição é gerada, não digitada

A identidade de um item é nome básico + conjunto de (característica, valor). O cadastrador preenche atributos; o sistema monta a descrição canônica. É o modelo do PDM do CATMAT, das properties do eCl@ss e dos attribute groups da Oracle — quatro instâncias independentes do mesmo padrão. É também a única defesa estrutural contra "PARAF. M8 40MM INOX" e "Parafuso sextavado M8x40" coexistirem como itens diferentes.

2. O que é do fornecedor não fica no item

Preço, prazo de entrega, quantidade mínima, múltiplo de compra, código do item no fornecedor e unidade de compra vivem em ItemSupplier. Coupa, SAP, Oracle e Ariba são unânimes nisso, e o motivo é simples: nada disso é verdade sobre o item — é verdade sobre uma relação. O primeiro segundo fornecedor quebra qualquer modelo que tenha colocado isso no item.

3. O cadastro guarda o que decide a compra, não o que apura tributo

Campos fiscais existem para conferir a nota do fornecedor e para classificar a intenção da compra. CST, CFOP, alíquotas, base reduzida, código de benefício e cClassTrib são responsabilidade do emitente e do ERP fiscal. Guardá-los aqui produz dado que estará errado numa fração relevante das operações.

4. Nenhum campo fiscal é obrigatório para criar um item

O item nasce de uma necessidade de compra, frequentemente antes de existir fornecedor. Exigir NCM no cadastro faz o usuário digitar 0000.00.00 — e dado errado é pior que dado ausente. Na UI, o rótulo é explícito: "NCM esperado — usado para conferir a nota do fornecedor".

5. A taxonomia classifica; ela não carrega atributos nem políticas

"Requer laudo", "exige DPO", "criticidade" e "com dado pessoal" nunca viram nós da árvore. Viram atributo, flag ou documento. O eCl@ss consegue ser fixo em quatro níveis porque escoa granularidade em 23 mil propriedades; sem esse escape, a granularidade vira nome de subcategoria e a árvore apodrece.

6. Uma unidade nunca basta

Nenhum dos seis produtos pesquisados tem "a UoM do item" — Coupa tem seis papéis de unidade, D365 e NetSuite têm três. E a conversão é por item: "1 CX = 12 UN" não é verdade sobre "CX", é verdade sobre este parafuso. Conversão global de classe quebra no primeiro tenant que compra caixa de 12 e caixa de 24 do mesmo produto.

7. Buscar antes de criar

A tela de novo item começa como busca, não como formulário em branco. É o passo mais barato e mais eficaz do fluxo do SAP MDG, e é onde a duplicidade morre — antes de nascer, não numa rotina de limpeza depois.

Modelo

Mapa de entidades

Sete agregados novos, um deles de configuração. O Item é o centro; tudo que varia por fornecedor, por categoria ou por operação fica fora dele.

ItemType configuração · por tenant Dirige quais campos existem, quais são obrigatórios e quais classes de unidade são válidas.
CategoryNode árvore · N níveis Classifica. Fornece o esquema de atributos, o NCM sugerido e as exigências regulatórias herdadas.
AttributeDefinition dicionário · por tenant Atributo tipado, com unidade e lista de valores. Ligado às categorias por CategoryAttribute.
UnitOfMeasure mestre · por classe Unidade dentro de uma classe (peso, volume, contagem, tempo, serviço). Conversão padrão só intraclasse.
Item aggregate root Filhos: ItemAttributeValue, ItemUomConversion, ItemPhoto, ItemKeyword, ItemDocument, ItemCompliance.
ItemSupplier aggregate root · N por item A relação de fornecimento: prazo, MOQ, múltiplo, unidade de compra, código no fornecedor, contrato, tolerâncias, preferência e status. Chaveado por item × fornecedor × estabelecimento do fornecedor, o último anulável.
ItemSupplierPrice filha da relação · N por relação Novo em 09/09/2026. A versão de preço, append-only: valor, moeda, unidade e quantidade de precificação, faixas e incoterm. Acrescenta à chave da relação a vigência, a UF de entrega, a empresa compradora e o incoterm.
ItemLifecyclePhase configuração · por tenant Fase do ciclo de vida e a matriz processo × política que ela impõe.
Estoque fora de escopo Saldo, depósito, lote, ponto de reposição. Ganchos previstos, nada implementado.
Nota recebida outro módulo Dados fiscais como declarados pelo emitente, imutáveis. É contra isso que o "esperado" do item é conferido.

O CostCenter não aparece no cadastro de item — a amarração contábil acontece na requisição, não no catálogo. Ver Impacto na aprovação.

O Item é dono de anexo desde 09/09/2026 (ownerType = Item): a evidência avulsa do cadastro — laudo, catálogo do fabricante, e-mail de especificação. Foto e ficha técnica continuam entidade do catálogo (ItemPhoto, ItemDocument), com perfil próprio no GrydFiles: arquivo com metadado seu é entidade de domínio, arquivo que é só prova é Attachment. As onze respostas do descritor estão declaradas na página do Anexo, em O dono polimórfico — este módulo implementa, não redecide.

Convenção de nomes. O documento é em português; os identificadores são em inglês — entidades, atributos, valores de enum, rotas, permissões e códigos de erro. A exceção são os dados de negócio brasileiros, que ficam como são no mundo real: identificadores fiscais (cnpj, cpf, primaryCnae), nomes de regime tributário (SimplesNacional, LucroReal), siglas de unidade (UN, CX, KG), tipos de certidão e os exemplos de taxonomia — esses são conteúdo, não código.

Configuração · ItemType

Tipo de item

ItemType não é um enum: é entidade de configuração por tenant, seguindo o padrão do material type do SAP e do item model group do D365. O tipo dirige field selection — quais grupos de campos aparecem, quais são obrigatórios e o que o domínio recusa.

Campos do ItemType

CampoTipoEfeito
code / namestring(30) / string(80)Código único por tenant, imutável. Nome exibido.
isSystemboolTipos-semente não podem ser excluídos, só desativados ou renomeados.
requiresGoodsTaxCodeboolLibera o grupo fiscal de mercadoria (NCM, CEST, GTIN) e o grupo físico (peso, volume, dimensões).
requiresServiceTaxCodeboolLibera o grupo fiscal de serviço (LC 116, cTribNac, NBS). Mutuamente exclusivo com o anterior.
allowsRecurrenceboolLibera periodicidade e unidade de cobrança.
isCapexboolLiga requiresAssetTag por default e habilita vida útil.
controlsStockboolgancho Reservado. Hoje sempre false; libera lote e validade quando estoque entrar.
allowedUomClassesUomClass[]Impede "hora" em matéria-prima e "quilograma" em serviço de consultoria.
defaultPredominantUseenumDestinação sugerida no cadastro (ver Fiscal).
requiredFieldsstring[]Field selection: campos opcionais no domínio que este tipo torna obrigatórios.
isActiveboolTipo inativo não aceita novos itens; itens existentes não são afetados.

Tipos-semente por tenant

CódigoFiscalRecorrênciaCAPEXClasses de unidadeDestinação padrão
RAW_MATERIALMercadoria——Count, Weight, Volume, Length, AreaProductionInput
FINISHED_GOODMercadoria——Count, Weight, VolumeResale
MROMercadoria——Count, Weight, Volume, LengthConsumableUse
PACKAGINGMercadoria——Count, Weight, AreaProductionInput
CAPEX_ASSETMercadoria—simCountFixedAsset
SERVICEServiço——Time, Service, AreaConsumableUse
RECURRING_SERVICEServiçosim—Time, ServiceConsumableUse
SOFTWARE_SUBSCRIPTIONServiçosim—Service, TimeConsumableUse
RENTALServiçosim—Time, Service, CountConsumableUse

Por que tipo configurável e não enum. O tipo de um item não é estável: MRO vira contrato de manutenção, software perpétuo vira assinatura. Migrar entre entidades é caro; mudar um ItemTypeId é barato — desde que o domínio revalide os campos condicionais na troca (ver RN-ITM-06).

Domínio · Item

Campos do item

Legenda de obrigatoriedade: obrig sempre · cond conforme ItemType ou categoria · opc opcional · gerado calculado pelo sistema.

Identificação

CampoTipoRegra
internalCodestring(16)geradoFormato CAT-{YYYY}-{NNNNN}, único por tenant, imutável. Gerado por IItemCodeService.
externalCodestring(100)?opcionalReferência legada de exibição. Integração/upsert usa ExternalReference único por tenant, origem, empresa externa, tipo e identificador; ambiguidade é pendência.
itemTypeIdGuidobrigDeve referenciar tipo ativo do tenant.
gtinstring(14)opc8, 12, 13 ou 14 dígitos com dígito verificador válido. Único por tenant quando informado. Recusado se requiresServiceTaxCode.
manufacturerNamestring(160)opcFabricante ou marca. Não confundir com fornecedor.
mpnstring(100)opcPart number do fabricante. O par (manufacturerName, mpn) normalizado é único por tenant.

Classificação e descrição

CampoTipoRegra
categoryNodeIdGuidobrigDeve ser nó ativo, vigente e com allowsPosting — folha, por padrão. Ver RN-ITM-11.
basicNamestring(60)obrigNome básico do item, equivalente ao INC do CATMAT. Sem modificadores: PARAFUSO, não PARAFUSO M8 INOX.
standardDescriptionstring(200)geradoMontada a partir de basicName + atributos marcados como usedInDescription, na ordem definida. Recalculada a cada alteração de atributo. Digitável apenas quando a categoria não tem esquema de atributos.
technicalDescriptionstring(1000)opcTexto livre complementar. Nunca canônico, nunca usado em matching.
unspscCodestring(8)opcMapeamento para taxonomia externa. Só para benchmark e exportação — não participa de nenhuma regra.
keywordsstring(50)[]opcMáx. 10. Entram na busca full-text.

Unidades e características físicas

CampoTipoRegra
baseUomIdGuidobrigUnidade canônica do item. Toda comparação de preço normaliza para ela. A classe da unidade deve constar em ItemType.allowedUomClasses. Imutável após a primeira transação: requisição, cotação, pedido ou recebimento.
defaultPurchaseUomIdGuidopcDefault = base. Precisa ter conversão cadastrada para a base.
conversionsItemUomConversion[]opcVer Unidades e conversão.
netWeight / netWeightUomIddecimal(18,6)condSó quando requiresGoodsTaxCode. Unidade da classe Peso.
grossWeight / grossWeightUomIddecimal(18,6)condIdem. Usado em frete e carga.
length / width / height / dimensionUomIddecimal(18,6)condIdem. Unidade da classe Comprimento, comum às três medidas.

Comercial

CampoTipoRegra
referencePricedecimal(18,4)opcBenchmark exibido no comparativo de cotações. Exige manage:catalog-prices.
referencePriceCurrencystring(3)condISO 4217. Obrigatório quando há preço. Deve estar entre as moedas ativas do tenant.
referencePriceUomIdGuidcondEm qual unidade o preço está. Default = base. Preço sem unidade é número sem significado.
referencePriceUpdatedAtdatetimegeradoExibido junto ao preço. Preço de referência de dois anos atrás é ruído, não benchmark.

Recorrência — quando allowsRecurrence

CampoTipoRegra
defaultBillingPeriodenumcondMonthly | Quarterly | SemiAnnual | Annual | OnDemand.
billingUnitstring(40)opcBase de cobrança: usuário-mês, licença, host, GB. Texto controlado por lista do tenant.

Vigência real, data de renovação, aviso prévio e regra de reajuste não ficam no item — ficam no contrato e são copiados para a linha do pedido. A mesma licença tem vigências diferentes por fornecedor; renewalDate no item master é dado que nasce errado.

Política de compra

CampoTipoRegra
isPurchasableboolobrigSeparado de "existe". Item pode existir no catálogo e não ser requisitável hoje.
requiresInspectionboolopcMarca o recebimento como sujeito a inspeção.
requiresAssetTagboolopcDefault herdado de ItemType.isCapex, sobrescrevível.
requiresContractboolopcItem só pode virar pedido sob contrato vigente.
allowsPartialReceiptboolopcDefault true.
requiresNationalContentboolopcFlag de negócio para política de conteúdo nacional. Não é o código de origem da NF-e.
criticalityenum?opcStrategic | Leverage | Bottleneck | Routine (matriz de Kraljic). Alimenta analytics e pode entrar como critério de aprovação.
accessGroupIdsGuid[]opcVazio = visível a todos. Restringe quem enxerga e requisita o item.

Ciclo de vida e controle

CampoTipoRegra
approvalStatusenumgeradoDraft | PendingApproval | Approved | Rejected. Muda só por comandos de transição. PendingApproval é uma instância aberta no motor de aprovação (processo ITEM); Approved e Rejected são o efeito dela — o item guarda approvalRequestId.
reasonCodeId · reasonCode · reasonLabel · reasonCommentGuid? · string(40)? · string(120)? · string(1000)?condA quádrupla do Motivo (RN-RSN-02), gravada na rejeição da instância ITEM: a chave para agrupar, mais código e rótulo congelados, mais o complemento em texto. Código e comentário são obrigatórios na rejeição — o código classifica, o comentário explica aquele caso —, e o uso declarado pelo processo é ItemReject. Substitui em 08/09/2026 o campo rejectionReason · string(500) · mínimo 20 caracteres, que era o texto livre que esta própria página já dizia ter aposentado em dois outros lugares: a tabela de transições exige motivo codificado desde 04/09/2026, e o código de erro se descreve como substituto do ITEM_REJECTION_REASON_REQUIRED. A tabela de campos era o último resto, e com ele um mínimo de vinte caracteres impossível de cumprir quando o motivo escolhido dispensa comentário.
lifecyclePhaseIdGuidobrigFase configurável por tenant. Traz a matriz processo × política.
processPolicyOverridesmapopcSobrescreve a política da fase para este item específico.
successorItemIdGuid?opcItem substituto. Exibido ao requisitante quando o item está em descontinuação.
validFrom / validTodate?opcVigência do cadastro. Fora da vigência, o item não aparece na requisição.
predominantUseenumobrigVer Fiscal. Default vem do ItemType.
defaultGlAccountIdGuid?opcNovo. Conta contábil sugerida para este item na fatia de rateio. É o primeiro degrau da cascata de sugestão; o segundo é a categoria. Sugestão, nunca imposição — quem tem permissão troca na linha. Ver Empresa e Estabelecimento.
versionintgeradoNasce 1, incrementa a cada update. Token de concorrência do PUT.
completenessScoreintgeradoPercentual de atributos obrigatórios e recomendados preenchidos. Indicador de governança, exibido na listagem.

Coleções filhas

ColeçãoLimiteRegra
attributeValuesesquema da categoriaUm valor por AttributeDefinition, ou N quando o atributo é multivalorado.
conversions20Uma vigente por unidade de destino — as versões anteriores continuam na coleção com validTo fechado, porque documento antigo precisa continuar explicável. Não pode conter a unidade base.
photos5JPG, PNG, WEBP. storedFileId → GrydFiles, perfil nexio.item-photo; tamanho e tipo vêm do perfil, e a URL pré-assinada é emitida na hora. ItemPhoto cria a sua FileReference com ownerScope = "nexio.item-photo".
documents20FISPQ, certificado, laudo, manual. Tipo, versão, data de emissão, validade e storedFileId → GrydFiles, perfil nexio.item-document, ownerScope = "nexio.item-document". Documento vencido gera alerta e pode bloquear a compra. ItemDocument é entidade do domínio, não Attachment — tem número, órgão e validade, e é a regra de corte da spec de Anexo.
compliances10Registro regulatório: órgão, número, validade. Ver Regulatório.
keywords1050 caracteres cada.

Fornecedores preferidos não são mais uma coleção do item — viraram o agregado ItemSupplier, com preço, prazo e status próprios.

Domínio · CategoryNode

Taxonomia

Um único tipo, auto-relacionado por parentId. Nenhuma taxonomia de referência de compras tem dois níveis — UNSPSC, eCl@ss, GPC e CATMAT têm quatro; as spend taxonomies corporativas convergem em três a quatro. E nenhum produto pesquisado usa profundidade fixa: Ariba, Coupa, Dynamics e Oracle usam todos lista de adjacência.

A mecânica da árvore é a do Centro de Custo, revisada em 08/09/2026 para ser idêntica nas três árvores do produto: caminho materializado em texto, depth, duas portas de status com cascata assimétrica, move só por PATCH /parent com path como guarda, version como token e teto de profundidade como política do tenant. Nada disso depende do banco — a arquitetura ainda não o escolheu. O que distingue a categoria das outras duas árvores é a carga do nó: conta sugerida, NCM, tipo default, exigências regulatórias e esquema de atributos.

CampoTipoRegra
parentIdGuid?Nulo = raiz. Ciclo é recusado. Alterado apenas por PATCH /{id}/parent. Profundidade validada contra catalogMaxCategoryDepth, parâmetro da configuração por tenant do Nexio — TenantSettings não existe na plataforma; pendência registrada no mapa de domínio.
codestring(30)Único por tenant. Estável — é o que a importação e as regras de aprovação referenciam.
name / descriptionstring(120) / string(500)Nome único entre irmãos.
pathvarchargerado Revisado em v3.9. Cadeia de códigos com separador nas duas pontas: /FIXADORES/PARAFUSOS/ — o mesmo formato do Centro de Custo e do plano de contas. Ancestrais saem da própria string; descendentes, de um prefixo escapado. Nenhuma leitura de árvore custa uma consulta por nível, e nenhum tipo específico de banco. Recalculado em cascata ao mover um nó. Guarda de move concorrente; o token de escrita é version.
depthintgerado Revisado em v3.9. Raiz é 1; publicado como level = depth - 1, como nas outras duas árvores. Até a v3.8 level era 1-based — mesmo nome público, valor diferente do Centro de Custo.
versionintgerado Novo em v3.6. Nasce 1 e incrementa a cada escrita. Token de concorrência de toda escrita da categoria — PUT, PATCH /parent, PATCH /activate e DELETE exigem expectedVersion no corpo; divergência é 409 CATEGORY_VERSION_CONFLICT. Coluna inteira comum, sem tipo específico de banco. Mesmo mecanismo do Item e, desde 03/09/2026, das três árvores; path é guarda de move, não token.
sortOrderintOrdem de exibição entre irmãos.
validFrom / validTodate? / date?Novo em v3.9. Vigência opcional, separada de isActive: nulo = sem prazo. Vigência é fato de calendário — uma categoria de campanha ou de safra termina; isActive continua sendo decisão administrativa. Fora da vigência o nó não é oferecido a item novo nem a linha nova, e continua legível no histórico. Filho não excede a vigência do pai. Mesma mecânica do Centro de Custo e do plano de contas.
allowsPostingboolgerado Novo em v3.9. Nasce true em folha e false quando o nó ganha o primeiro filho. Editável por exceção, nó a nó, por quem tem manage:catalog-categories. Só nó com allowsPosting aceita item — é a regra "só folha" com uma válvula declarada, o mesmo campo e o mesmo sentido do rateio no centro de custo e do lançamento na conta.
defaultGlAccountIdGuid?Novo. Conta contábil sugerida para os itens desta categoria. A busca sobe a árvore: o nó mais próximo com conta preenchida vence, o que permite amarrar a conta em qualquer nível e não repetir em toda folha — é o padrão de mapping set da Oracle.
suggestedNcmstring(8)?Pré-preenche o NCM esperado dos itens da categoria. Sugestão, nunca imposição.
defaultItemTypeIdGuid?Pré-seleciona o tipo ao criar item nesta categoria.
requiredComplianceKindsenum[]Exigências regulatórias herdadas por todos os itens da subárvore. Ex.: categoria "EPI" exige CA.
isActiveboolgerado Nasce true. Muda só por PATCH /activate e DELETE, em cascata assimétrica: ativar exige a cadeia de ancestrais ativa e não desce; desativar desce à subárvore inteira. O corpo do PUT não o carrega. Categoria inativa não aceita novos itens; itens existentes não são afetados.

Profundidade livre no armazenamento, teto na política

maxDepth é configuração do tenant, default 2 — a UI segue exibindo dois selects encadeados e nada muda para quem já usa. Regras de aprovação e mapeamentos contábeis operam sobre qualquer nível. É o padrão da Oracle, que permite criar mais de dez níveis mas só amarra política nos dez primeiros: desacopla "quantos níveis o cliente desenha" de "quantos o motor entende".

Herança para baixo, override no filho

Esquema de atributos, NCM sugerido, tipo default e exigências regulatórias são herdados do ancestral mais próximo que os define. O nó filho pode acrescentar, nunca remover o que o pai marcou como obrigatório.

Item só classifica em nó que aceita item — folha, por padrão

allowsPosting nasce verdadeiro em folha e falso quando o nó ganha o primeiro filho. Sem essa regra, metade do catálogo fica pendurada em nós intermediários e a agregação por categoria mente. A exceção existe e é declarada nó a nó, como no centro de custo: o tenant que cria PREDIAL e INDUSTRIAL sob MANUTENÇÃO pode manter os itens antigos no pai enquanto reclassifica aos poucos — marcando a exceção e assumindo o relatório por categoria que deixa de fechar. Sem a exceção, criar um filho sob um nó que já tem itens continua exigindo a reclassificação desses itens antes de concluir — com sugestão automática por atributos.

Migração a partir de Category / Subcategory

Cada Category vira nó raiz (depth = 1) com parentId = null; cada Subcategory vira nó de depth = 2 apontando para ela. allowsPosting nasce da contagem de filhos e a vigência nasce aberta, como na migração do Centro de Custo. Item.subcategoryId vira categoryNodeId; categoryId é descartado, já que o path o reconstrói. Categoria sem subcategorias ganha um filho GERAL para satisfazer a regra da folha. Migração determinística, sem decisão humana.

Migration · substitui o agregado Category e a entidade Subcategory

Domínio · AttributeDefinition

Atributos por categoria

É o mecanismo que faz "parafuso precisa de bitola, rosca e material" e "notebook precisa de processador, RAM e tela" conviverem sem explodir a taxonomia. Quatro implementações independentes chegaram ao mesmo desenho — eCl@ss properties, SAP characteristics, Oracle attribute groups por item class e o PDM do CATMAT.

Definição do atributo

CampoTipoRegra
code / namestring(40) / string(80)Código único por tenant, imutável.
dataTypeenumText | LocalizedText | Integer | Decimal | Measure | Currency | Boolean | Date | List | MultiList | Reference. Imutável após o primeiro valor gravado.
uomClassIdGuid?Obrigatório em Measure. É o que faz 3/8 pol e 9,525 mm serem comparáveis.
minValue / maxValue / decimalsdecimal? / intValidação de faixa para tipos numéricos.
valuesAttributeValue[]Lista de valores codificados, para List e MultiList.
isRestrictiveboolLista fechada (só os valores cadastrados) ou sugerida (aceita valor novo, que entra na lista). "Cor da tinta" é fechada; "acabamento" pode ser aberta.
isMultiValuedboolPermite N valores no mesmo atributo. Ex.: certificações: ISO 9001, ISO 14001.

Vínculo categoria × atributo

CampoTipoRegra
categoryNodeId / attributeDefinitionIdGuidChave composta.
isRequiredboolItem não é aprovado sem os obrigatórios preenchidos.
usedInDescriptionboolParticipa da descrição gerada.
descriptionOrderint?Posição na descrição gerada. Obrigatório quando usedInDescription.
displayOrderintOrdem no formulário.
Exemplo · descrição gerada
categoria      FIXADORES › PARAFUSOS › SEXTAVADOS
basicName      PARAFUSO
atributos      tipo_cabeca   = Sextavado          (ordem 1)
               diametro      = M8                 (ordem 2, Measure)
               comprimento   = 40 mm              (ordem 3, Measure)
               material      = Aço inox 304       (ordem 4, List restritiva)
               rosca         = Parcial            (ordem 5, List restritiva)

standardDescription  →  "PARAFUSO SEXTAVADO M8 X 40MM AÇO INOX 304 ROSCA PARCIAL"
attributeSignature   →  sha256("PARAFUSO|7f3c…c1|comprimento=0.04:MT|diametro=0.008:MT
                                |material=INOX304|rosca=PARCIAL|tipo_cabeca=SEXTAVADO")

Revisto em 09/09/2026. A assinatura é o hash de três coisas: o basicName normalizado, o categoryNodeId e os atributos obrigatórios da categoria, ordenados por código. Os dois primeiros entram porque o princípio 1 define a identidade do item como nome básico mais atributos — sem eles, "PARAFUSO M8 aço inox" e "PORCA M8 aço inox" em categorias diferentes podem colidir, e a camada 4 bloquearia um item que não é duplicata de nada. Valor de Measure entra convertido para a unidade base da classe, nunca na unidade digitada: 3/8 pol e 9,525 mm são a mesma bitola e precisam produzir o mesmo hash. Duas pessoas preenchendo os mesmos valores produzem a mesma assinatura — e a duplicata é detectada antes de existir. Ver RN-ITM-31.

Categoria sem esquema continua funcionando

Nem todo tenant vai desenhar PDM no primeiro dia. Categoria sem atributos vinculados aceita standardDescription digitada, e o completenessScore reflete isso. O esquema é uma evolução da qualidade do cadastro, não um pré-requisito de adoção.

E, sem esquema, a camada 4 da antiduplicidade não existe — não há o que assinar. O item nasce com attributeSignature nula, a camada 4 é pulada e as camadas 1, 2, 3 e 5 continuam valendo; a de trigram sobre a descrição é o que segura o dia 1. Isto está dito porque o caso é o comum, não o excepcional: fingir que a assinatura existe é prometer um bloqueio que nunca dispara.

Mudar o esquema recalcula as assinaturas

Alternar CategoryAttribute.isRequired muda o conjunto que a assinatura resume — e assinaturas gravadas antes deixam de ser comparáveis com as gravadas depois. Por isso a alteração dispara o recálculo da assinatura de todos os itens do nó e da subárvore, em tarefa assíncrona, e a camada 4 fica suspensa naquele ramo até o recálculo terminar. A tarefa é idempotente e o seu término é auditado. Vincular ou desvincular um atributo obrigatório tem o mesmo efeito. É a mesma escolha da árvore: mecanismo declarado em vez de promessa na prosa.

Atributo não vira coluna

Valores vivem em ItemAttributeValue com colunas tipadas (valueText, valueNumber, valueBool, valueDate, valueListId) mais uomId. Nenhum produto pesquisado modela atributo como coluna do item — com taxonomias distintas por tenant, seria inviável.

Domínio · UnitOfMeasure

Unidades e conversão

Esta é a seção que paga ou cobra no comparativo de cotações. Se o fornecedor A cota R$ 120 / CX(12 UN) e o B cota R$ 11 / UN, o comprador precisa ver R$ 10,00/UN vs R$ 11,00/UN sem calculadora.

Classes de unidade

ClasseBase sugeridaConvertívelObservação
CountUNsimDZ, MIL, PC, PAR convertem pelo fator canônico. CX, PT, FD, SC, RL, KIT e JG são embalagem (isPackaging): não têm fator canônico e só convertem por item ou por fornecedor — o que cabe na caixa depende do que está dentro dela.
WeightKGsimG, TON. Conversão padrão global.
VolumeLsimML, M3.
LengthMsimMM, CM, KM, POL.
AreaM2simCM2, HA.
TimeHsimMIN, DIA, MES. Conversão global só onde é inequívoca.
ServiceVBnãoVerba, entrega, marco, ponto de função. Classe não conversível — ver abaixo.

Conversão por item, com numerador e denominador

ItemUomConversion (itemId, uomId, numerator, denominator) sobrepõe a conversão global da classe. O par inteiro evita o erro de arredondamento que um fator decimal introduz — 1 MIL = 1000 UN em inteiros, não 0,001. O NetSuite escolheu conversão global no nível do Units Type e documenta as consequências: não é possível editar o tipo depois de atribuído nem trocar o tipo de um item. SAP (MARM) e Oracle fazem por item, e a Oracle é explícita ao exigir que o item exista antes de criar conversões.

A classe Serviço não converte, nem entre si

Não existe "1 mês = X horas" universal. Duas propostas de consultoria — uma em horas, outra em verba global — não têm denominador comum. O sistema recusa a comparação numérica e sinaliza, em vez de estimar. Forçar preço por unidade base em serviço produz número sem significado.

A cotação guarda o original e o normalizado

A linha de cotação carrega (priceAmount, qty, quotedUomId, declaredFactor) e o sistema deriva pricePerBaseUom. Guardar só o normalizado perde a informação original e impede auditoria; guardar só o original impede comparação. O declaredFactor é o que o fornecedor afirma caber na embalagem — se divergir do cadastro do item, o comparativo sinaliza em vez de calcular errado em silêncio.

Sem conversão cadastrada, sem comparação

Se um fornecedor cota em KG e outro em UN e não há conversão interclasse declarada naquele item, o comparativo exibe as duas propostas lado a lado sem preço normalizado, com aviso explícito, e a submissão que dependesse do fator responde 422. Estimar seria pior que não comparar. A conversão interclasse é permitida e é do item (RN-ITM-32) — o que o sistema nunca faz é inventá-la.

A unidade base é imutável depois da primeira transação

Trocar a base reescreve o significado de todo o histórico de preço e de todo o comparativo já emitido. Se for realmente necessário, o caminho é criar item sucessor e marcar o atual como substituído.

Contrato vigente · revisão consolidada

Relação, oferta e preço

ItemSupplier preserva o relacionamento de qualificação e preferência. ItemSupplierOffer identifica uma apresentação comercial versionada: o mesmo fornecedor pode vender caixa de 12, caixa de 24 e unidade avulsa simultaneamente. ItemSupplierPrice referencia a versão da oferta, com condição comercial e vigência próprias.

EntidadeCamposInvariantes
ItemSupplierid, tenantId, itemId, supplierId, supplierEstablishmentId?, status, isPreferred, priority, reviewByDate, versionÚnica por (tenantId, itemId, supplierId, supplierEstablishmentId), tratando nulo como igual. Estabelecimento exato vence o genérico. Status New, UnderQualification, Approved ou Blocked; qualificação vigente do fornecedor continua sendo gate adicional. Máximo de 10 relações preferidas por item.
ItemSupplierOfferid, itemSupplierId, offerKey, revision, supplierPartNumber, supplierAuxPartNumber?, purchaseUomId, numerator, denominator, supplierUomLabel?, moq?, orderIncrement?, standardQuantity?, leadTimeDays?, validFrom, validTo?, contractLineId?, overTolerancePct?, underTolerancePct?, versionIdentidade comercial estável offerKey e revisão imutável após uso. MOQ, múltiplo e padrão na unidade de compra. Fatores positivos; prazo estimado não substitui promessa de entrega. Várias ofertas simultâneas; nunca escolher embalagem arbitrariamente.
ItemSupplierPriceid, itemSupplierOfferId, validFrom, validTo?, companyId?, shipToStateCode?, commercialConditionKey, incoterm?, incotermNamedPlace?, priceAmount, currency, priceUomId, priceQty, priceTiersPreço por versão da oferta; priceAmount numeric(19,10), priceQty numeric(19,6), moeda ativa. Máximo 10 faixas, ordenadas sem sobreposição. Unidade de preço pode diferir da compra, com fator congelado. Reajuste cria versão; edição apenas antes do uso.
ItemSupplierPriceComponentid, priceId, kind, amount, currency, basis, includedInPrice, recoverability, sourceRef, observedAtCusto explícito de mercadoria, frete, seguro, embalagem, ferramental, tributo não recuperável e outros custos justificados. Total numeric(21,4); base por unidade, lote ou pedido. Recuperabilidade confirmada pelo fiscal/ERP; desconhecida não é crédito presumido.

Resolver relação por estabelecimento exato e depois genérico; selecionar a oferta explicitamente; dentro dela, a condição comercial solicitada e o preço vigente. Prioridade total: empresa exata antes da genérica; dentro de cada uma, UF exata antes da genérica. Nunca consultar preço de outra empresa. Impedir cruzamento de vigência para a mesma oferta, empresa, UF e commercialConditionKey, com nulos equivalentes e trava de publicação.

Incoterm é condicional. Aplicável quando a compra de mercadoria adota regras Incoterms, com local nomeado e modal compatíveis. Serviço e assinatura podem ter incoterm nulo; frete doméstico declarado não é automaticamente FOB ou CIF internacional. O código não substitui os componentes do custo total. A ICC define regras para entrega de mercadorias; não são uma classificação universal de todo preço.

UF é um critério inicial de tabela, não a afirmação de que dois endereços na mesma cidade custam igual. Oferta/proposta deve explicitar frete e restrições para o destino real. Divergência entre condição genérica e destino impede tratar o valor como custo final. Contrato tem revisão e linha referenciadas; não sobrepõe silenciosamente preço ou quantidade de pedido emitido.

Código comercial identifica a oferta no fornecedor; ofertas alternativas podem compartilhar item e relação. Evitar duplicidade por fornecedor, estabelecimento, código, variante e identidade da oferta, permitindo revisões históricas. Referências de ERP usam ExternalReference de Contratos transacionais, e não um externalCode ambíguo. A linha do documento congela relação, oferta/revisão, preço, componentes, quantidade original, unidade, ambos os fatores e condições.

Brasil

Fiscal e regulatório

O Nexio é o lado do comprador: a maioria dos atributos fiscais não é dado que se cadastra, é dado que chega na nota. Cada campo abaixo está em um de três baldes.

cadastrar

Atributo estável do item, necessário antes de comprar — para especificar, comparar, exigir ou bloquear.

esperar

Vem no XML da nota. O Nexio armazena como dado observado e usa para conferência. Não se cadastra.

não carregar

Responsabilidade do ERP fiscal. Guardar aqui produz dado errado e cria duas fontes de verdade.

Mercadorias — quando requiresGoodsTaxCode

CampoBaldeRegra no Nexio
expectedNcmcadastrar8 dígitos, validado contra a tabela NCM espelhada. Rotulado como NCM esperado. Pré-preenchido pelo suggestedNcm da categoria. Divergência contra a nota vira alerta de conferência.
expectedCestcadastrar7 dígitos, opcional, exige NCM preenchido. Relevante só para tenants que revendem. A relação NCM→CEST é 1:N e se resolve pela descrição — o autocomplete sugere, o humano escolhe.
gtincadastrarJustificado por antiduplicidade, matching de linha de nota e leitura por coletor. A obrigação fiscal é do emitente; aqui é valor de negócio.
Origem da mercadoria (0–8)esperarDepende de como o emitente adquiriu — o mesmo produto sai com origem 1 pelo importador e 2 pelo distribuidor. É atributo do par item×fornecedor observado, não do item. Para política de conteúdo nacional existe a flag requiresNationalContent.
cEANTrib, uTrib, qTribnão carregarCampos de emissão. O conceito — unidade de compra × unidade base × fator — está modelado em Unidades; o campo fiscal, não.
cProdANPesperarSetorial (combustíveis). Vem na nota.

Serviços — quando requiresServiceTaxCode

CampoBaldeRegra no Nexio
serviceCodeLc116cadastrarSubitem da lista da LC 116/2003 (ex.: 7.02). Estrutura de dois níveis, ~200 subitens ativos após as alterações até a LC 218/2025.
cTribNaccadastrarCódigo-âncora recomendado. Os 338 códigos do Código de Tributação Nacional da NFS-e desdobram a LC 116 de forma padronizada — é o primeiro código de serviço nacionalmente comparável, o que viabiliza benchmark entre tenants de municípios diferentes.
nbsCodecadastrarReferência opcional de serviço. Exigibilidade depende do documento, operação e leiaute fiscal vigente; não presumir obrigatoriedade futura sem norma.
cTribMunnão carregarVaria por município do prestador. Não é atributo do item.
Alíquotas e regras de retençãonão carregarRetenção depende de quatro variáveis e o item carrega apenas uma. Ver o quadro abaixo.

Retenções dependem da natureza do serviço, prestador, regime, forma de execução e regras aplicáveis à operação. O item fornece classificação, e pedido/contrato descrevem execução. O ERP/fiscal determina incidência e alíquotas vigentes; não codificar uma alíquota universal no catálogo.

O item guarda o código de classificação. A matriz de retenção vive no motor fiscal. O que o Nexio deve oferecer é a flag cessaoDeMaoDeObra na linha do pedido ou no contrato — porque isso é decisão de compra, negociada, e ninguém a jusante sabe disso melhor que o comprador.

Reforma tributária — o único campo que nasce no comprador

CampoBaldeRegra no Nexio
predominantUsecadastrarProductionInput | Resale | ConsumableUse | FixedAsset | PersonalBenefit. Default do ItemType, sobrescrevível na linha do pedido. O art. 57 da LC 214/2025 pergunta se o bem se destina à atividade econômica ou ao consumo pessoal — o fornecedor não sabe, o ERP fiscal não sabe, o sistema de compras sabe: está no requisitante, no centro de custo e na justificativa.
cClassTribnão carregarA armadilha de 2026. O mesmo NCM tem cClassTrib diferentes conforme a descrição precisa e o tipo de operação — absorvente e fralda descartável compartilham NCM e têm códigos distintos. Não é derivável do cadastro de item. Armazenar apenas o observado na nota.
CST IBS/CBS, alíquotas, reduçõesesperarInsumo de custo real, vindo da nota.
CST/CSOSN, CFOP, alíquotas ICMS/IPI/PIS/COFINS, cBenef, FCInão carregarCFOP é 100% da operação. Alíquota é função de origem, destino, regime, produto, benefício e data — guardar no item é garantir número errado. Custo real vem da nota recebida.

O parser preserva IBS/CBS quando informados e tolera ausência conforme a versão vigente do leiaute e regras de autorização. A comunicação oficial de agosto de 2026 anunciou flexibilização; cronograma e obrigatoriedade não devem ser fixados a partir de uma data anterior. Cada integração registra versão do esquema e regra fiscal aplicada.

Regulatório — ItemCompliance e ItemDocument

Quase nada disso é "um campo no item". A regra de decisão: flag quando a política é universal e binária; atributo de categoria quando é específica de um tipo de produto; objeto com validade quando tem número, órgão emissor e vencimento; documento anexo quando o que importa é o arquivo.

CasoMecanismoOnde mora
EPI — Certificado de AprovaçãoItemComplianceNº do CA, órgão MTE, validade. Bloqueia a compra quando vencido — é vedado ao empregador fornecer EPI sem CA. O caso mais forte de campo estruturado.
Registro ANVISA (medicamento, saneante, correlato)ItemComplianceNº de registro do produto no item. A AFE é do fornecedor, não do item.
Registro MAPA (fertilizante, defensivo, veterinário)ItemComplianceNº de registro no item. Receituário agronômico é documento da requisição — tem validade curta e é por aplicação.
Produto controlado — Exército / PF / Polícia Civilflag + habilitaçãocontrolKind no item (None | FederalPolice | Army | StatePolice). CR, licença e mapas mensais são habilitação do tenant e do fornecedor, com validade. Nunca no item.
Produto perigoso — FISPQItemDocument + camposFISPQ anexada com versão e data. Extraídos como campos: número ONU e classe de risco — porque disparam regra de transporte e armazenagem.
Certificação INMETRO / homologação ANATELItemComplianceNº do certificado, órgão, validade, arquivo anexo.
Exigência de laudo ou certificado na entregarequiredDocumentKindsLista herdada da categoria. O recebimento cobra os documentos declarados.

ItemCompliance: kind, agency, registrationNumber, issuedAt, expiresAt, documentId? (continua apontando para ItemDocument, nunca para o arquivo), blocksPurchaseWhenExpired. Um job diário varre vencimentos e notifica os responsáveis pelo catálogo via GrydNotifications.

Governança

Ciclo de vida

Três eixos ortogonais, seguindo a Oracle, que separa Status, Lifecycle Phase e Approval Status como atributos distintos. Colapsar os três num único enum é o erro que obriga a inventar estados como "ativo mas em revisão e bloqueado para pedido".

Eixo 1 — aprovação do cadastro

DeParaComandoRegra
—DraftPOST /itemsQuem tem create:catalog. Validação mínima: nome básico, tipo, categoria, unidade base.
DraftPendingApprovalPOST /items/{id}/submitExige atributos obrigatórios da categoria preenchidos e re-checagem de duplicidade. Abre a instância ITEM no motor, com categoria, tipo de item e as flags do item (regulado, dado pessoal, crítico) como critérios.
PendingApprovalApprovedtarefa do motorDecisão na instância (POST /approvals/{id}/assignments/{aid}/approve). A política padrão semeada aponta para quem tem approve:catalog-items; o aprovador vê os candidatos a duplicata antes de decidir. Até a v3.6 existia POST /items/{id}/approve.
PendingApprovalRejectedtarefa do motorRejeição na instância. Motivo codificado e comentário obrigatórios — uso ItemReject, declarado pelo processo (RN-AP-30); o comentário passou a ser sempre exigido, como em toda rejeição no motor (RN-AP-19). O item grava a quádrupla de motivo e notifica o criador. Ver Motivo.
RejectedDraftPUT /items/{id}Editar um item rejeitado o devolve a rascunho.
ApprovedPendingApprovalPUT /items/{id} → revaluateRevisto em 09/09/2026 — esta página deixou de ter regra própria de reabertura. Alteração que toca campos materiais (categoria, tipo, unidade base, atributos obrigatórios, identificadores) chama revaluate na instância; quem reabre é o motor, que faz Approved → InApproval, incrementa o roundNumber e emite o evento de reabertura. O item acompanha a instância e volta a PendingApproval. Continua sendo desta página quais campos são materiais; o que acontece depois deles é do motor. Alterações menores não reabrem aprovação. Ver Fluxo de Aprovação.

Eixo 2 — fase do ciclo de vida

Configurável por tenant (ItemLifecyclePhase). Fases-semente: New, Active, PhasingOut, Obsolete, Superseded. Fase não é status de aprovação nem bloqueio — é onde o item está na vida dele.

Eixo 3 — matriz processo × política

Cada fase declara, por processo, uma das três políticas do modelo do D365: permitido, aviso ou bloqueado.

FaseRequisiçãoCotaçãoPedidoRecebimento
Newbloqueadopermitidobloqueadobloqueado
Activepermitidopermitidopermitidopermitido
PhasingOutavisoavisopermitidopermitido
Obsoletebloqueadobloqueadobloqueadopermitido
Supersededbloqueadobloqueadobloqueadopermitido

Ortogonais não quer dizer independentes na hora de comprar — novo em 09/09/2026. Os três eixos respondem perguntas diferentes e por isso não colapsam num enum só. Mas virar linha de documento exige os três ao mesmo tempo: approvalStatus = Approved, fase que permite ou avisa aquele processo, e isPurchasable verdadeiro. Faltando qualquer um, a linha é recusada com 422 ITEM_NOT_PURCHASABLE_UNPROCESSABLE, e a mensagem diz qual dos três recusou — porque "item indisponível" manda o requisitante para a pessoa errada. Sem esta regra, um item Rejected pela curadoria em fase Active era requisitável, cotável e pedível pelas regras escritas, e a curadoria de catálogo — que tem processo próprio no motor, fila, SLA e motivo codificado — não bloqueava nada. Ver RN-ITM-30.

Recebimento permanece permitido em todas as fases finais: bloquear o recebimento de um pedido legítimo já colocado transforma uma decisão de catálogo num problema operacional. E Superseded sempre exibe o successorItemId ao requisitante — bloquear sem oferecer alternativa gera compra fora do processo.

Qualidade de dados

Antiduplicidade

Seis camadas, três pontos de disparo. O modelo de dois limiares vem do SAP MDG: abaixo do limiar baixo nada é sinalizado; entre os dois, apresenta-se como candidato; no ou acima do alto, considera-se idêntico — e ainda assim se apresenta ao usuário, nunca se decide sozinho.

#Regra de matchAção
1Exato em (manufacturerName, mpn) normalizado — maiúsculas, sem hífen, sem espaçobloqueia
2Exato em gtinbloqueia
3Exato em (supplierId, supplierPartNumber)bloqueia na relação ItemSupplier — nunca na versão de preço, e é por isso que reajustar não colide com a própria regra (RN-ITM-26)
4attributeSignature — hash de basicName + categoryNodeId + atributos obrigatórios da categoria, com valor Measure normalizado para a unidade base da classe (RN-ITM-31)bloqueia — não se aplica em categoria sem esquema de atributos, onde a assinatura não existe
5Similaridade trigram (pg_trgm no Postgres, como exemplo de implementação; onde não houver, a similaridade é calculada na aplicação) sobre standardDescription, dentro da mesma categoria, com dicionário de abreviações pt-BR (PARAF→PARAFUSO, GALV→GALVANIZADO, INOX→AÇO INOXIDÁVEL)alerta com score e candidatos
6Re-execução das camadas 4 e 5 na submissão e na aprovaçãoalerta ao aprovador

A tela de novo item começa como busca

POST /catalog/items/duplicate-check recebe nome básico, categoria e atributos parciais e devolve candidatos antes de qualquer gravação. É o passo mais barato do fluxo e o que mais reduz duplicata — bloquear na submissão já é tarde, o usuário já investiu no cadastro.

Bloqueio tem escape auditado

Quem tem approve:catalog-items pode forçar a criação de um item que casou em camada bloqueante, com motivo codificado obrigatório — uso ItemDuplicateOverride, cujo código semeado exige complemento em texto — registrado no ato e no GrydAudit. Frequência alta neste uso é sinal de regra de duplicidade mal calibrada, e agora isso é contável. Existem casos legítimos — mesmo MPN com especificações divergentes entre fabricantes homônimos. Bloqueio sem escape gera cadastro criativo: "PARAFUSO M8 (2)".

Rastreabilidade

Regras de negócio

RN-ITM-01

Código interno gerado e imutável

Formato CAT-{YYYY}-{NNNNN}, único por tenant, sem edição manual. Reservado no momento da criação, mesmo em rascunho, para não haver furo de sequência.

RN-ITM-02

Todo item tem tipo, categoria que aceita item e unidade base

Os três são obrigatórios já no rascunho. É o mínimo que torna o item comparável, classificável e cotável.

RN-ITM-03

A classe da unidade base deve ser permitida pelo tipo

ItemType.allowedUomClasses é validado no domínio. Impede matéria-prima medida em horas e consultoria medida em quilos.

RN-ITM-04

Campos condicionais são recusados fora do seu tipo

NCM, CEST, GTIN, peso e dimensões em item de serviço, ou código LC 116 em matéria-prima, respondem 400 ITEM_FIELD_NOT_APPLICABLE com o nome do campo e do tipo. Ignorar silenciosamente é pior — o usuário acha que gravou.

RN-ITM-05

Nenhum campo fiscal é obrigatório para criar item

Nem para aprovar. O tenant pode elevar campos a obrigatórios via ItemType.requiredFields, mas o default do produto é opcional.

RN-ITM-06

Troca de tipo revalida os campos condicionais

Ao mudar itemTypeId, o domínio verifica os campos preenchidos contra o novo tipo. Campos que deixam de se aplicar são listados na resposta e a operação só prossegue com discardIncompatibleFields: true explícito. Os valores descartados ficam no log de auditoria.

RN-ITM-07

A descrição padronizada é derivada

Recalculada a cada alteração de basicName ou de atributo marcado como usedInDescription. Só é editável quando a categoria não tem esquema de atributos. Truncada em 200 caracteres com aviso quando os atributos geram texto maior.

RN-ITM-08

Atributos obrigatórios da categoria bloqueiam a submissão

Não bloqueiam a criação do rascunho — bloqueiam submit. Cadastrar em duas sessões é um caso real; aprovar item incompleto não é.

RN-ITM-09

Conversão de unidade é por item, com par inteiro

numerator e denominator inteiros positivos. A unidade base não pode figurar como destino. Conversão entre classes distintas existe, e só por item — nunca global, nunca herdada da conversão padrão. É o caso do aço em barra comprado por SC e cobrado por TN, e do cabo comprado em MT e cobrado em KG: quem declara a equivalência é o item, porque 1 MT de cabo pesa o que aquele cabo pesa e nada disso vale para "cabo" em geral. Corrigido em 09/09/2026 — ver RN-ITM-32.

RN-ITM-10

A unidade base é imutável após o primeiro documento

Requisição, cotação ou pedido referenciando o item congelam a base. Alteração responde 409 ITEM_BASE_UOM_LOCKED_CONFLICT e sugere criar item sucessor.

RN-ITM-11

Item só classifica em nó ativo, vigente e com allowsPosting

Revisada em 08/09/2026. allowsPosting nasce true em folha e false quando o nó ganha o primeiro filho; a exceção é editável nó a nó. Item apontando para nó sem a flag responde 400 ITEM_CATEGORY_NOT_POSTABLE; fora da vigência, 422 ITEM_CATEGORY_NOT_EFFECTIVE_UNPROCESSABLE. Criar filho sob um nó que tem itens e não tem a exceção marcada exige reclassificar esses itens na mesma transação: a API devolve 409 CATEGORY_NODE_HAS_ITEMS_CONFLICT com a contagem, e o comando aceita um mapa de reclassificação — ou a exceção.

RN-ITM-12

Profundidade da árvore respeita o teto do tenant

Revisto em 08/09/2026: na v1 não há teto — a profundidade da árvore de categorias é livre, como a do centro de custo. catalogMaxCategoryDepth é gancho declarado: campo opcional deste documento, padrão nulo, validado no POST e no PATCH /parent quando existir valor. Aumentar o teto é operação de admin e não afeta nada existente; diminuir é recusado enquanto houver nós mais profundos. Enquanto o valor for nulo, nada é recusado e o erro correspondente não é devolvido — ele fica declarado, para nascer junto com o teto. Por quê: até 08/09 esta página validava contra um campo que nenhuma spec definia, e o erro era devolvido contra um teto que ninguém podia configurar; a saída foi tirar a promessa, não inventar a entidade — o único consumidor é esta validação, e o padrão é "sem limite".

RN-ITM-13

Categoria e tipo inativos não recebem novos itens

Itens existentes não são afetados nem reclassificados. Ao desativar, a resposta informa a contagem de itens ativos atingidos.

RN-ITM-14

Item não é excluído fisicamente

Havendo requisição, cotação, pedido ou nota vinculada, só há mudança de fase. O DELETE existe apenas para itens em Draft nunca submetidos.

RN-ITM-15

Fornecedor bloqueado não apaga o vínculo

SupplierStatusChangedEvent muda o ItemSupplier para Blocked, preservando preço, histórico e prioridade. Reaprovação do fornecedor restaura o vínculo no status anterior.

RN-ITM-16

Compliance vencido bloqueia a compra quando declarado

blocksPurchaseWhenExpired impede requisição e pedido a partir do vencimento. Job diário notifica 30, 15 e 5 dias antes. É o comportamento exigido para EPI sem CA válido.

RN-ITM-17

Destinação predominante é obrigatória e sobrescrevível

Default vem do ItemType. A linha do pedido pode sobrescrever, e a sobrescrita é auditada — é evidência da intenção no momento da compra, defensável em fiscalização.

RN-ITM-18

Preço de referência exige moeda e unidade

Os três campos gravam juntos ou nenhum grava. Preço sem unidade não é comparável e vira ruído no comparativo de cotações.

RN-ITM-19

Concorrência otimista em todo update

expectedVersion obrigatório no corpo do PUT, checado antes de tocar o agregado. Versão defasada responde 409 ITEM_VERSION_CONFLICT com a versão atual na mensagem. Vale igual para ItemSupplier e, desde a v3.6, para CategoryNode — no PUT, no PATCH /parent, no PATCH /activate e no DELETE. É o token único do produto; nas três árvores path é guarda de move, não token.

RN-ITM-20

Auditoria completa com diff de campos

Item, ItemSupplier, CategoryNode e AttributeDefinition são IAuditableEntity, categoria Catalog. Registram campo, valor anterior, valor novo, usuário, IP e timestamp.

RN-ITM-21Destino integra a condição comercial

UF é critério inicial de tabela, usando snapshot da linha. Oferta/proposta declara frete e restrições para o endereço real; custo pode variar dentro da mesma UF. Empresa exata precede a genérica e depois UF exata precede a genérica, sem aproximação silenciosa.

RN-ITM-22

A categoria tem duas portas de status, com cascata assimétrica

Nova em v3.9. PATCH /activate exige a cadeia de ancestrais ativa e vigente e não desce à subárvore; DELETE desativa a subárvore inteira e recusa se houver item ativo no nó ou abaixo (409 CATEGORY_NODE_HAS_ITEMS_CONFLICT). Criar, mover ou ativar sob ramo inativo responde 422 CATEGORY_ANCESTOR_INACTIVE_UNPROCESSABLE. O corpo do PUT não carrega isActive. Uma linha de auditoria por cascata, no nó disparado — a mesma mecânica do Centro de Custo.

RN-ITM-23

Vigência da categoria é calendário, não status

Nova em v3.9. validFrom e validTo são opcionais e independentes de isActive. Filho não excede a vigência do pai (422 CATEGORY_VALIDITY_OUTSIDE_PARENT_UNPROCESSABLE); encerrar um pai encerra a subárvore na mesma data, em cascata. Nó fora da vigência não é oferecido a item novo nem a linha nova e continua legível no histórico.

RN-ITM-24Preço é versão da oferta

ItemSupplierPrice referencia itemSupplierOfferId. Unicidade e vigência por oferta, companyId, shipToStateCode e commercialConditionKey, tratando nulos como iguais. Reajuste cria versão; seleção e snapshot usam a data de submissão da revisão. Incoterm é atributo condicional, não chave obrigatória universal.

RN-ITM-25Oferta usada não muda de significado

Fator, código comercial, embalagem, MOQ e condições relevantes de oferta usada exigem nova revisão; mudança física de embalagem identifica nova apresentação (por exemplo CX10). ItemUomConversion usada também é versionada. O documento conserva fatores originais. A convenção CX10 é do produto, não obrigação fiscal universal.

RN-ITM-26Três identidades comerciais

ItemSupplier identifica a relação; ItemSupplierOffer, a apresentação comercial e sua revisão; ItemSupplierPrice, a condição e o preço vigente da oferta. Status e preferência são da relação; embalagem, código, mínimo e prazo são da oferta; valores e componentes são do preço.

RN-ITM-27Custo total explícito e Incoterm condicional

Incoterm exige operação aplicável, local e modal coerentes. Pode ser nulo em serviço e assinatura. Componentes de custo declaram base, inclusão, moeda, recuperabilidade e origem; não somar frete já incluído nem deduzir crédito apenas pelo regime do fornecedor.

RN-ITM-28Preço empresarial desde a v1

companyId restringe a versão de preço; nulo é condição genérica explícita. Resolver empresa exata antes da genérica e, dentro dela, UF exata antes da genérica, para oferta e condição escolhidas. Controle empresarial não é apenas gancho futuro.

RN-ITM-29Fatores têm direção declarada

convNum/convDen converte quantidade pedida para base; priceConvNum/priceConvDen converte base para unidade de preço. Compra e preço em CX de 12, base UN: 12/1 e 1/12. Só unidade de preço igual à base usa 1/1. Valor usa quantidade original e ambos os fatores, sem arredondar a quantidade intermediária.

RN-ITM-30Cadastro atual governa novos compromissos

Novas requisições, cotações e pedidos exigem item aprovado, comprável e fase compatível, além dos gates de fornecedor. Alteração cadastral não impede registrar entrega/aceite de pedido vigente pelo snapshot. Revalidar gate ao assumir compromisso, não apenas na submissão antiga.

RN-ITM-31

A assinatura inclui nome e categoria, e mede na unidade base

Nova em 09/09/2026. attributeSignature = sha256(basicName normalizado | categoryNodeId | atributos obrigatórios ordenados por código), com todo valor de Measure convertido para a unidade base da classe antes de entrar no hash — 3/8 pol e 9,525 mm são a mesma bitola. Nome e categoria entram porque o princípio 1 define a identidade do item como os dois juntos, e sem eles a camada 4 bloqueia itens que não são duplicata um do outro. Alternar CategoryAttribute.isRequired, ou vincular e desvincular atributo obrigatório, dispara recálculo assíncrono das assinaturas do nó e da subárvore, com a camada 4 suspensa naquele ramo até terminar. Categoria sem esquema não tem assinatura — a assinatura é nula, a camada 4 é pulada e as outras cinco continuam valendo.

RN-ITM-32

Conversão interclasse é do item, e o item precisa declarar a equivalência

Nova em 09/09/2026. conversions[] aceita unidade de destino de outra classe, e só quando o item declara a equivalência naquela linha — é conversão do item, não da unidade. Sem isso o caso que esta especificação usa três vezes como prova de que o modelo funciona — aço em SC cobrado em TN, cabo em MT cobrado em KG — não era cadastrável, porque a regra da coleção exigia a mesma classe da base. Continuam valendo: a classe Service não converte, nem entre si; a conversão global permanece estritamente intraclasse; e nada de interclasse é herdado — o que o item não declarou, o sistema recusa com 422, nunca estima. Ver Unidade de Medida.

Camada API

Endpoints

Prefixo api/v{version}/nexio. Todos exigem [Authorize] e uma permissão explícita via [RequirePermission]. Sucesso é 200, inclusive nos POST, seguindo o padrão já estabelecido no módulo Admin.

Item

RotaPermissãoRetornoErros
GET /catalog/itemsread:catalogPaged de ItemSummaryDto400
GET /catalog/items/{id}read:catalogItemDto completo, com atributos, conversões, documentos e compliance404
POST /catalog/itemscreate:catalogItemDto em Draft400 · 409 · 422
PUT /catalog/items/{id}update:catalogItemDto400 · 404 · 409
POST /catalog/items/duplicate-checkread:catalogLista de candidatos com score e camada de match. Sem efeito colateral.400
POST /catalog/items/{id}/submitcreate:catalogItemDto em PendingApproval, com approvalRequestId. Abre a instância ITEM no motor409 · 422
sem rota própria—Aprovar e rejeitar é decisão do motor, na tarefa da instância; o item aplica Approved ou Rejected ao terminar. Até a v3.6: POST /catalog/items/{id}/approve · /reject—
PUT /catalog/items/{id}/lifecycle-phaseupdate:catalogItemDto400 · 404 · 409
POST /catalog/items/{id}/cloneclone:catalogNovo item em Draft, sem código interno do original404
DELETE /catalog/items/{id}delete:catalogSem conteúdo. Só rascunho nunca submetido404 · 409
GET /catalog/items/{id}/auditread:catalog-auditPaged de eventos com diff de campos404

Coleções do item

RotaPermissãoObservação
PUT /catalog/items/{id}/attributesupdate:catalogReescreve o conjunto inteiro. Recalcula descrição e assinatura.
PUT /catalog/items/{id}/uom-conversionsupdate:catalogVersiona, não reescreve (RN-UOM-09): fator diferente para uma unidade já usada fecha a linha vigente com validTo e grava a nova com validFrom. Nada é apagado, e o fator de uma linha já usada por documento é imutável.
POST /catalog/items/{id}/photos/upload-intentupdate:catalogDevolve fileId, uploadUrl pré-assinada, uploadExpiresAt e os cabeçalhos do PUT, no perfil nexio.item-photo. Recusa antes de reservar quando o item já tem 5 fotos. O binário nunca passa pela API — RN-GF-06 do GrydFiles; a rota multipart da v3.2 saiu em 03/09/2026.
POST /catalog/items/{id}/photosupdate:catalogConfirma o fileId no GrydFiles, cria o ItemPhoto e a sua FileReference. Idempotente por fileId. Máx. 5 no total.
DELETE /catalog/items/{id}/photos/{photoId}update:catalogApaga o ItemPhoto e libera a FileReference. Não chama DeleteAsync do storage: o Nexio nunca apaga arquivo, e o perfil nexio.item-photo morre por contagem de referência, na carência do GrydFiles. Corrigido em 08/09/2026 — era o resto da v3.2 apontado no M11.
POST /catalog/items/{id}/documentsupdate:catalogTipo, versão, emissão e validade obrigatórios.
PUT /catalog/items/{id}/compliancesupdate:catalogReescreve o conjunto.
GET /catalog/items/{id}/suppliersread:catalogPaged, filtrável por status.
POST /catalog/items/{id}/suppliersmanage:catalog-suppliersPreço exige manage:catalog-prices adicionalmente.
PUT /catalog/items/{id}/suppliers/{supplierItemId}manage:catalog-suppliersConcorrência por expectedVersion.
DELETE /catalog/items/{id}/suppliers/{supplierItemId}manage:catalog-suppliersApaga a relação e as versões de preço dela. Só se a relação nunca foi usada em cotação ou pedido; caso contrário, bloquear. Decidível desde 09/09/2026 — antes, "nunca usado" não tinha sujeito.
GET /catalog/items/{id}/suppliers/{supplierItemId}/offers/{offerId}/pricesread:catalogNovo em 09/09/2026. Versões de preço da relação, da mais recente para a mais antiga. Filtro onDate devolve a vigente naquela data; sem ele, o histórico inteiro.
POST /catalog/items/{id}/suppliers/{supplierItemId}/offers/{offerId}/pricesmanage:catalog-pricesNovo em 09/09/2026. Grava versão nova e fecha a anterior da mesma combinação em validTo. Reajuste passa por aqui — não existe rota que edite preço vigente. Recusa cruzamento de vigência; exige incoterm apenas quando aplicável à operação.
PUT /catalog/items/{id}/suppliers/{supplierItemId}/offers/{offerId}/prices/{priceId}manage:catalog-pricesNovo em 09/09/2026. Corrige uma versão ainda não usada em documento — erro de digitação, não reajuste. Versão já congelada em linha responde 409.

Configuração do catálogo

RotaPermissãoObservação
GET /catalog/categoriesread:catalogÁrvore completa ou subárvore via rootId + levels. Não paginada — é árvore, não lista.
POST /catalog/categoriesmanage:catalog-categoriesValida profundidade, nome único entre irmãos e ramo ativo. Se o pai tem itens e não tem a exceção de allowsPosting, exige o mapa de reclassificação (RN-ITM-11).
PUT /catalog/categories/{id}manage:catalog-categoriesRenomeia, reordena e edita a carga do nó. Não move nem muda status — parentId e isActive não estão no corpo. expectedVersion obrigatório.
PATCH /catalog/categories/{id}/parentmanage:catalog-categoriesNovo em v3.9. Move o nó e recalcula path e depth da subárvore em cascata, em transação. Valida ciclo, teto de profundidade, ramo ativo e nome único entre os novos irmãos. path é guarda de move; expectedVersion obrigatório.
PATCH /catalog/categories/{id}/activatemanage:catalog-categoriesNovo em v3.9. Reativa o nó. Exige a cadeia de ancestrais ativa e vigente; não desce à subárvore. expectedVersion obrigatório.
PATCH /catalog/categories/{id}/deactivatemanage:catalog-categoriesDesativa a subárvore em cascata. Recusa se houver item ativo no nó ou abaixo. expectedVersion obrigatório.
PUT /catalog/categories/{id}/attributesmanage:catalog-categoriesVincula atributos e define obrigatoriedade e ordem na descrição.
GET /catalog/attribute-definitionsread:catalogPaged, filtrável por tipo de dado.
POST /catalog/attribute-definitionsmanage:catalog-attributesdataType imutável após o primeiro valor gravado.
GET /catalog/item-typesread:catalogConsumido pelo formulário para decidir campos visíveis.
POST /catalog/item-typesmanage:catalog-item-typesrequiresGoodsTaxCode e requiresServiceTaxCode são mutuamente exclusivos.
GET /catalog/uomsread:uomsMódulo UoM. Filtro por classe, consumido pelo autocomplete.
GET /catalog/ncmread:catalogTabela NCM espelhada, busca full-text sobre a descrição concatenada. Global, não por tenant.

Listagem de itens — filtros e ordenação

FiltrosOrdenável porPadrão
search, categoryNodeId (+includeDescendants), itemTypeId, approvalStatus, lifecyclePhaseId, isPurchasable, supplierId, hasExpiredCompliance, completenessBelowinternalCode, standardDescription, updatedAt, completenessScoreupdatedAt desc

Página padrão 20, teto 100, valores fora da faixa ajustados. sortBy inválido responde 400 SORT_FIELD_INVALID com a lista permitida na mensagem. includeDeleted=true é recusado. A busca cobre standardDescription, basicName, internalCode, externalCode, mpn, gtin e keywords. As DTOs resolvem nomes de fornecedor e de categoria pelo diretório — nunca devolvem só Guids.

Contrato

Códigos de erro

Payload de erro: code, traceId e type, com mensagem resolvida em pt-BR, en e es-ES. Onde a tela precisa de dados para agir — lista de duplicatas, campos incompatíveis — ela consulta antes; o erro fica como guarda de corrida.

CódigoHTTPQuando
ITEM_NOT_FOUND404Item inexistente no tenant
ITEM_BASIC_NAME_REQUIRED / _TOO_LONG400Nome básico vazio ou > 60
ITEM_TYPE_REQUIRED / _INACTIVE400Tipo ausente ou desativado
ITEM_CATEGORY_NOT_POSTABLE400Categoria informada não aceita item — allowsPosting falso. Substitui ITEM_CATEGORY_NOT_LEAF desde a v3.9
ITEM_CATEGORY_NOT_EFFECTIVE_UNPROCESSABLE422Categoria fora da vigência
ITEM_BASE_UOM_REQUIRED400Unidade base ausente
ITEM_UOM_CLASS_NOT_ALLOWED400Classe da unidade não permitida pelo tipo
ITEM_FIELD_NOT_APPLICABLE400Campo preenchido que o tipo não usa — mensagem nomeia campo e tipo
ITEM_GTIN_INVALID400Comprimento ou dígito verificador inválido
ITEM_NCM_INVALID400NCM não existe na tabela vigente
ITEM_CEST_REQUIRES_NCM400CEST informado sem NCM
ITEM_PRICE_INCOMPLETE400Preço sem moeda ou sem unidade
ITEM_CONVERSION_INVALID400Numerador ou denominador ≤ 0, ou destino igual à base
ITEM_ATTRIBUTE_NOT_IN_SCHEMA400Atributo não pertence ao esquema da categoria
ITEM_ATTRIBUTE_VALUE_INVALID400Fora da faixa, fora da lista restritiva ou tipo incompatível
ITEM_DUPLICATE_CONFLICT409Match em camada bloqueante — mensagem traz o código interno do item existente
ITEM_VERSION_CONFLICT409Versão defasada no PUT
ITEM_BASE_UOM_LOCKED_CONFLICT409Unidade base já usada em documento
ITEM_INVALID_TRANSITION_CONFLICT409Transição de aprovação inválida a partir do status atual
ITEM_HAS_DOCUMENTS_CONFLICT409Exclusão de item com requisição, cotação, pedido ou nota
ITEM_REQUIRED_ATTRIBUTES_MISSING_UNPROCESSABLE422Submissão sem atributos obrigatórios — lista os códigos faltantes
REASON_CODE_REQUIRED_UNPROCESSABLE422Rejeição sem reasonCodeId, ou escape de duplicidade sem motivo. Substitui o antigo ITEM_REJECTION_REASON_REQUIRED, que era texto livre com mínimo de caracteres
CATEGORY_NODE_NOT_FOUND404Nó inexistente
CATEGORY_MAX_DEPTH_EXCEEDED400Reservado desde 08/09/2026. Profundidade acima do teto do tenant — só é devolvido quando catalogMaxCategoryDepth tiver valor, e na v1 ele é nulo
CATEGORY_CYCLE_DETECTED400Mover nó para dentro da própria subárvore
CATEGORY_SIBLING_NAME_ALREADY_EXISTS409Nome repetido entre irmãos
CATEGORY_NODE_HAS_ITEMS_CONFLICT409Criar filho sob nó com itens sem a exceção de allowsPosting, ou desativar nó com item ativo no nó ou abaixo
CATEGORY_VERSION_CONFLICT409Versão defasada no PUT, PATCH /parent, PATCH /activate ou DELETE da categoria — ou, num move, o path mudou desde a leitura
CATEGORY_ANCESTOR_INACTIVE_UNPROCESSABLE422Criar, mover ou ativar sob ramo inativo
CATEGORY_VALIDITY_OUTSIDE_PARENT_UNPROCESSABLE422Vigência do filho excede a do pai
ATTRIBUTE_DATA_TYPE_LOCKED_CONFLICT409Alterar tipo de dado com valores já gravados
ITEM_NOT_PURCHASABLE_UNPROCESSABLE422Novo em 09/09/2026. Item recusado como linha de documento — a mensagem diz qual dos três eixos recusou: aprovação, fase do ciclo de vida ou isPurchasable (RN-ITM-30)
ITEM_SUPPLIER_DUPLICATED_CONFLICT409Já existe relação para (item, fornecedor, estabelecimento do fornecedor). A mensagem nomeia as três partes e a relação que colide
ITEM_SUPPLIER_PRICE_DUPLICATED_CONFLICT409Preço sobrepõe vigência na mesma oferta, empresa, UF e commercialConditionKey.
ITEM_SUPPLIER_PRICE_INCOTERM_REQUIRED400Operação que adota Incoterms sem regra, local ou modal exigido. Serviço e assinatura sem Incoterm não produzem este erro.
ITEM_SUPPLIER_PRICE_NOT_EFFECTIVE_UNPROCESSABLE422Nenhum preço vigente para oferta e condição selecionadas, após precedência empresa → UF.
ITEM_SUPPLIER_PRICE_LOCKED_CONFLICT409Novo em 09/09/2026. Edição de versão de preço já congelada em linha de documento — reajuste é versão nova
ITEM_SUPPLIER_BLOCKED_CONFLICT409Fornecedor bloqueado no módulo Fornecedores
ITEM_SUPPLIER_SHIP_TO_STATE_INVALID400UF fora das 27 unidades federativas
ITEM_COMPLIANCE_EXPIRED_CONFLICT409Compra de item com compliance vencido e bloqueante

Autorização

Permissões

Padrão do produto, fechado em 08/09/2026 e revisto no fim do dia contra o código da plataforma: <verbo>:<recurso>[-<sub-recurso>] — verbo em kebab-case singular, recurso em kebab-case plural. O plural é de 09/09/2026 e alinha ao catálogo semeado do GrydAuth (read:users, update:tenants, create:permissions); até então o acervo inteiro estava no singular. Duas classes ficam no singular, porque não são coleção: o que nomeia o módulo (read:catalog — o tenant tem um catálogo, não N) e o que nomeia a trilha de auditoria (read:catalog-audit, read:supplier-audit — é uma trilha, não N auditorias). O PermissionCodeParser do GrydAuth lê o que vem antes do : como ação e monta o rótulo da tela de papéis quebrando em hífen, nunca em ponto — daí o sub-recurso com hífen. O recurso é declarado uma vez no controlador; o verbo vem do método HTTP por [AutoPermission] quando é CRUD, e é nomeado quando é ato de negócio (approve, import, clone). Um endpoint declara exatamente uma permissão; nada é implicado por outra.

PermissãoSituaçãoCobre
read:catalog · create:catalog · update:catalog · delete:catalogexisteCRUD do item
import:catalog · export:catalog · clone:catalogexisteImportação, exportação e clonagem
manage:catalog-categoriesexisteÁrvore de categorias e vínculo de atributos
manage:catalog-suppliersexisteItemSupplier
manage:catalog-pricesexistePreço de referência do item e as versões de preço do fornecedor (ItemSupplierPrice). Criar a relação é manage:catalog-suppliers; gravar preço nela exige esta, adicionalmente
read:catalog-auditexisteHistórico por registro
approve:catalog-itemsnovoForçar criação sobre duplicata bloqueante; e é o alvo da política padrão do processo ITEM — quem a tem recebe as tarefas de curadoria no motor
manage:catalog-attributesnovoDicionário de atributos
manage:catalog-item-typesnovoTipos de item e fases do ciclo de vida

Grupos sugeridos: Nexio - Catálogo (read/create/update/clone/import/export), Nexio - Curadoria de Catálogo (+ approve, category, attribute, itemtype, price) e Nexio - Consulta de Catálogo (read). Um teste compara o conjunto seedado pelo atributo com o exigido pelo [RequirePermission], como já é feito no módulo Admin.

Onboarding

Importação em massa

É o maior gargalo de adoção: o cliente chega com uma planilha de dezenas de milhares de linhas de texto livre. O desenho abaixo assume isso.

ColunaRegra
external_codeopcChave de upsert. Presente e existente = atualiza; ausente = cria.
item_typeobrigCódigo do tipo.
category_pathobrigCaminho completo separado por >: FIXADORES > PARAFUSOS > SEXTAVADOS. Nós ausentes são criados quando createMissingCategories: true, respeitando maxDepth.
basic_nameobrigNome básico.
descriptioncondUsado apenas quando a categoria não tem esquema de atributos.
base_uomobrigSímbolo da unidade, validado contra as classes permitidas pelo tipo.
attr:{codigo}condColunas dinâmicas por atributo do esquema. O template baixado já vem com as colunas da categoria escolhida.
gtin, mpn, manufactureropcParticipam da antiduplicidade.
ncm, cest, service_code, ctribnac, nbsopcValidados, nunca obrigatórios.
reference_price, currency, price_uomopcOs três juntos ou nenhum.
supplier_code, supplier_part_number, purchase_uom, purchase_factor, price, lead_time, moqopcCria o ItemSupplier na mesma linha. Repetir a linha com fornecedor diferente adiciona vínculo.

Duas passadas: simular, depois aplicar

POST /catalog/items:import?dryRun=true devolve o relatório completo — linhas válidas, erros por linha e campo, categorias que seriam criadas, duplicatas detectadas — sem gravar nada. O usuário corrige a planilha e reenvia sem o parâmetro. Importar 5.000 linhas e descobrir os erros depois é o caminho mais rápido para um catálogo sujo.

Erro por linha não interrompe

Linhas válidas entram; inválidas vão para o relatório com linha, campo e motivo. Limite de 10.000 linhas e 20 MB por arquivo, processamento assíncrono via GrydJobs, notificação por e-mail ao concluir com link para o relatório.

Itens importados nascem em rascunho

Salvo quando o usuário tem approve:catalog-items e marca autoApprove: true. Importação que aprova automaticamente por default transforma a planilha do cliente em verdade sem revisão. Carga com autoApprove não é aprovação: cria o item já Approved sem abrir instância no motor, com a permissão e a trilha da importação — exceção declarada em 04/09/2026, ver Fluxo de Aprovação, "O que o motor não decide".

Encadeamento

O que o item entrega ao fluxo de aprovação

O fluxo de aprovação é impactado por este cadastro em cinco pontos. Vale fixá-los agora, porque cada um é uma decisão que o motor de aprovação vai herdar pronta ou vai ter que improvisar.

O que o item forneceComo a aprovação usa
categoryNodeId + pathA regra amarra em qualquer nível da árvore e vale para os descendentes por rollup. Guardar o nó, nunca a lista expandida de folhas — assim subcategoria criada depois já nasce coberta pela política. O match é por prefixo de path, com a mesma função que lê o centro de custo e a conta contábil.
itemTypeIdDimensão de regra natural e barata: serviço e CAPEX quase sempre têm alçada diferente de material de consumo.
criticality e flags de políticarequiresContract, criticality e controlKind são candidatos a disparar aprovador funcional obrigatório — Jurídico em contrato, SESMT em produto controlado.
referencePriceInsumo de detecção de anomalia de preço: pedido muito acima da referência sinaliza ao aprovador, o que é a correção prescrita para rubber-stamping.
predominantUseSepara despesa de investimento na origem. É o dado que distingue alçada de OPEX de alçada de CAPEX sem depender do centro de custo.

A curadoria também é uma instância do motor. Desde 04/09/2026 o catálogo não tem rota própria de aprovar: submit abre o processo ITEM, o motor decide e o item aplica o efeito. Quem cura é avisado pelo motor, não por um slug deste módulo: a instância resolve o passo e dispara nexio.approval-assignment.assigned a quem tem a tarefa — o mesmo caminho de toda aprovação do produto. O catálogo só escreve slug próprio para o que o motor não cobre, como nexio.item.rejected, que avisa o criador depois do desfecho. A política padrão semeada é um nível só, para quem tem approve:catalog-items — o dia 1 se comporta como antes, com fila, SLA e delegação. O tenant que quiser "categoria regulada passa pela qualidade" escreve uma política complementar com o critério de flag, sem código. Princípio 7 e RN-AP-28 do Fluxo de Aprovação.

A pergunta que estava aberta aqui foi respondida no motor, que é onde ela morava: um pedido tem N linhas, cada uma com sua categoria e seu tipo, e o modelo antigo do ApprovalFlow tinha um CategoryId singular com o invariante de resolver para um fluxo só. Desde 04/09/2026 vale a RN-AP-14 do Fluxo de Aprovação: as regras são avaliadas por linha e por fatia, e a decisão é do documento inteiro, com fatias do mesmo centro de custo agregadas antes de gerar passo. É a resposta que o mercado dá, e ela ficou barata porque foi tomada antes de existir entidade de requisição.

Limites

Fora de escopo e decisões pendentes

Fora de escopo · com gancho
  • Estoque — saldo, depósito, ponto de reposição, lote, número de série, validade. ItemType.controlsStock existe e é sempre false.
  • Variantes de item — o par cor/tamanho de varejo. Hoje cada combinação é um item; o esquema de atributos já suporta a modelagem quando fizer sentido.
  • Lista técnica (BOM) — produto composto de matérias-primas, para MRP, custeio e beneficiamento (o pedido do produto acabado carregando a lista de componentes a remeter). Fora, e sem gancho por decisão (06/09/2026): o Nexio compra o componente, não explode o produto, e sem estoque não há remessa. A única regra que fica é negativa — Item não aponta para Item.
  • Kit de compra — "um clique adiciona N linhas" (kit de onboarding, kit de EPI). Não é item: é modelo de requisição, com gancho na spec da Requisição. O Ariba modela como catalog kit no catálogo; a Oracle, como lista de compras na requisição — e a segunda forma é a que respeita o que este documento decidiu sobre fiscal, duplicidade e ItemSupplier. O kit vendido como SKU único, com NCM único, é um item comum.
  • Componentes de custo do preço: contrato mínimo definido nesta versão em ItemSupplierPriceComponent; automação de tributação e cálculo de custo de estoque permanecem no ERP.
  • Catálogo de fornecedor externo — punchout, CIF, cXML. O ItemSupplier foi desenhado para receber isso depois.
  • Tradução de itens — Ariba e Coupa modelam i18n do item como entidade filha. Fica previsto, não implementado.
Decisões pendentes
  • Compartilhamento de esquema entre tenants. Vale entregar um esquema-semente por setor, ou cada tenant desenha o seu do zero? O primeiro reduz o custo de onboarding; o segundo evita impor taxonomia errada.
  • Extração assistida na importação. Mapear texto livre para atributos é tarefa em que LLM é excelente e verificável. Entra no MVP da importação ou depois?
  • Mapeamento categoria → conta contábil. resolvido em 01/09/2026: GLAccount especificada, e a sugestão desce por Item.defaultGlAccountId e depois CategoryNode.defaultGlAccountId subindo a árvore. Ver Empresa e Estabelecimento.
  • Multiempresa: catálogo é compartilhado pelo tenant; oferta e preço têm condições por empresa desde a v1, e operação segue concessões empresariais.

Contrato vigente · revisão consolidada

Contratos de manutenção de ofertas

A relação mantém suas rotas de status e preferência. Acrescentar consulta e publicação de /catalog/items/{id}/suppliers/{supplierItemId}/offers, com revisão, validade e expectedVersion. Preços são mantidos sob /offers/{offerId}/prices; a rota anterior de preços da relação é substituída por este contrato antes de implementar. manage:catalog-suppliers governa ofertas; manage:catalog-prices governa preços e componentes. Sem endpoints implementados nesta revisão documental.