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.
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.
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.
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.
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.
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.
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.
CategoryAttribute.
ItemAttributeValue, ItemUomConversion, ItemPhoto, ItemKeyword, ItemDocument, ItemCompliance.
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
| Campo | Tipo | Efeito |
|---|---|---|
| code / name | string(30) / string(80) | Código único por tenant, imutável. Nome exibido. |
| isSystem | bool | Tipos-semente não podem ser excluídos, só desativados ou renomeados. |
| requiresGoodsTaxCode | bool | Libera o grupo fiscal de mercadoria (NCM, CEST, GTIN) e o grupo físico (peso, volume, dimensões). |
| requiresServiceTaxCode | bool | Libera o grupo fiscal de serviço (LC 116, cTribNac, NBS). Mutuamente exclusivo com o anterior. |
| allowsRecurrence | bool | Libera periodicidade e unidade de cobrança. |
| isCapex | bool | Liga requiresAssetTag por default e habilita vida útil. |
| controlsStock | bool | gancho Reservado. Hoje sempre false; libera lote e validade quando estoque entrar. |
| allowedUomClasses | UomClass[] | Impede "hora" em matéria-prima e "quilograma" em serviço de consultoria. |
| defaultPredominantUse | enum | Destinação sugerida no cadastro (ver Fiscal). |
| requiredFields | string[] | Field selection: campos opcionais no domínio que este tipo torna obrigatórios. |
| isActive | bool | Tipo inativo não aceita novos itens; itens existentes não são afetados. |
Tipos-semente por tenant
| Código | Fiscal | Recorrência | CAPEX | Classes de unidade | Destinação padrão |
|---|---|---|---|---|---|
| RAW_MATERIAL | Mercadoria | — | — | Count, Weight, Volume, Length, Area | ProductionInput |
| FINISHED_GOOD | Mercadoria | — | — | Count, Weight, Volume | Resale |
| MRO | Mercadoria | — | — | Count, Weight, Volume, Length | ConsumableUse |
| PACKAGING | Mercadoria | — | — | Count, Weight, Area | ProductionInput |
| CAPEX_ASSET | Mercadoria | — | sim | Count | FixedAsset |
| SERVICE | Serviço | — | — | Time, Service, Area | ConsumableUse |
| RECURRING_SERVICE | Serviço | sim | — | Time, Service | ConsumableUse |
| SOFTWARE_SUBSCRIPTION | Serviço | sim | — | Service, Time | ConsumableUse |
| RENTAL | Serviço | sim | — | Time, Service, Count | ConsumableUse |
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
| Campo | Tipo | Regra | |
|---|---|---|---|
| internalCode | string(16) | gerado | Formato CAT-{YYYY}-{NNNNN}, único por tenant, imutável. Gerado por IItemCodeService. |
externalCode | string(100)? | opcional | Referência legada de exibição. Integração/upsert usa ExternalReference único por tenant, origem, empresa externa, tipo e identificador; ambiguidade é pendência. |
| itemTypeId | Guid | obrig | Deve referenciar tipo ativo do tenant. |
| gtin | string(14) | opc | 8, 12, 13 ou 14 dígitos com dígito verificador válido. Único por tenant quando informado. Recusado se requiresServiceTaxCode. |
| manufacturerName | string(160) | opc | Fabricante ou marca. Não confundir com fornecedor. |
| mpn | string(100) | opc | Part number do fabricante. O par (manufacturerName, mpn) normalizado é único por tenant. |
Classificação e descrição
| Campo | Tipo | Regra | |
|---|---|---|---|
| categoryNodeId | Guid | obrig | Deve ser nó ativo, vigente e com allowsPosting — folha, por padrão. Ver RN-ITM-11. |
| basicName | string(60) | obrig | Nome básico do item, equivalente ao INC do CATMAT. Sem modificadores: PARAFUSO, não PARAFUSO M8 INOX. |
| standardDescription | string(200) | gerado | Montada 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. |
| technicalDescription | string(1000) | opc | Texto livre complementar. Nunca canônico, nunca usado em matching. |
| unspscCode | string(8) | opc | Mapeamento para taxonomia externa. Só para benchmark e exportação — não participa de nenhuma regra. |
| keywords | string(50)[] | opc | Máx. 10. Entram na busca full-text. |
Unidades e características físicas
| Campo | Tipo | Regra | |
|---|---|---|---|
| baseUomId | Guid | obrig | Unidade 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. |
| defaultPurchaseUomId | Guid | opc | Default = base. Precisa ter conversão cadastrada para a base. |
| conversions | ItemUomConversion[] | opc | Ver Unidades e conversão. |
| netWeight / netWeightUomId | decimal(18,6) | cond | Só quando requiresGoodsTaxCode. Unidade da classe Peso. |
| grossWeight / grossWeightUomId | decimal(18,6) | cond | Idem. Usado em frete e carga. |
| length / width / height / dimensionUomId | decimal(18,6) | cond | Idem. Unidade da classe Comprimento, comum às três medidas. |
Comercial
| Campo | Tipo | Regra | |
|---|---|---|---|
| referencePrice | decimal(18,4) | opc | Benchmark exibido no comparativo de cotações. Exige manage:catalog-prices. |
| referencePriceCurrency | string(3) | cond | ISO 4217. Obrigatório quando há preço. Deve estar entre as moedas ativas do tenant. |
| referencePriceUomId | Guid | cond | Em qual unidade o preço está. Default = base. Preço sem unidade é número sem significado. |
| referencePriceUpdatedAt | datetime | gerado | Exibido junto ao preço. Preço de referência de dois anos atrás é ruído, não benchmark. |
Recorrência — quando allowsRecurrence
| Campo | Tipo | Regra | |
|---|---|---|---|
| defaultBillingPeriod | enum | cond | Monthly | Quarterly | SemiAnnual | Annual | OnDemand. |
| billingUnit | string(40) | opc | Base 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
| Campo | Tipo | Regra | |
|---|---|---|---|
| isPurchasable | bool | obrig | Separado de "existe". Item pode existir no catálogo e não ser requisitável hoje. |
| requiresInspection | bool | opc | Marca o recebimento como sujeito a inspeção. |
| requiresAssetTag | bool | opc | Default herdado de ItemType.isCapex, sobrescrevível. |
| requiresContract | bool | opc | Item só pode virar pedido sob contrato vigente. |
| allowsPartialReceipt | bool | opc | Default true. |
| requiresNationalContent | bool | opc | Flag de negócio para política de conteúdo nacional. Não é o código de origem da NF-e. |
| criticality | enum? | opc | Strategic | Leverage | Bottleneck | Routine (matriz de Kraljic). Alimenta analytics e pode entrar como critério de aprovação. |
| accessGroupIds | Guid[] | opc | Vazio = visível a todos. Restringe quem enxerga e requisita o item. |
Ciclo de vida e controle
| Campo | Tipo | Regra | |
|---|---|---|---|
| approvalStatus | enum | gerado | Draft | 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 · reasonComment | Guid? · string(40)? · string(120)? · string(1000)? | cond | A 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. |
| lifecyclePhaseId | Guid | obrig | Fase configurável por tenant. Traz a matriz processo × política. |
| processPolicyOverrides | map | opc | Sobrescreve a política da fase para este item específico. |
| successorItemId | Guid? | opc | Item substituto. Exibido ao requisitante quando o item está em descontinuação. |
| validFrom / validTo | date? | opc | Vigência do cadastro. Fora da vigência, o item não aparece na requisição. |
| predominantUse | enum | obrig | Ver Fiscal. Default vem do ItemType. |
| defaultGlAccountId | Guid? | opc | Novo. 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. |
| version | int | gerado | Nasce 1, incrementa a cada update. Token de concorrência do PUT. |
| completenessScore | int | gerado | Percentual de atributos obrigatórios e recomendados preenchidos. Indicador de governança, exibido na listagem. |
Coleções filhas
| Coleção | Limite | Regra |
|---|---|---|
| attributeValues | esquema da categoria | Um valor por AttributeDefinition, ou N quando o atributo é multivalorado. |
| conversions | 20 | Uma 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. |
| photos | 5 | JPG, 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". |
| documents | 20 | FISPQ, 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. |
| compliances | 10 | Registro regulatório: órgão, número, validade. Ver Regulatório. |
| keywords | 10 | 50 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.
| Campo | Tipo | Regra |
|---|---|---|
| parentId | Guid? | 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. |
| code | string(30) | Único por tenant. Estável — é o que a importação e as regras de aprovação referenciam. |
| name / description | string(120) / string(500) | Nome único entre irmãos. |
| path | varchar | gerado 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. |
| depth | int | gerado 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. |
| version | int | gerado 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. |
| sortOrder | int | Ordem de exibição entre irmãos. |
| validFrom / validTo | date? / 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. |
| allowsPosting | bool | gerado 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. |
| defaultGlAccountId | Guid? | 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. |
| suggestedNcm | string(8)? | Pré-preenche o NCM esperado dos itens da categoria. Sugestão, nunca imposição. |
| defaultItemTypeId | Guid? | Pré-seleciona o tipo ao criar item nesta categoria. |
| requiredComplianceKinds | enum[] | Exigências regulatórias herdadas por todos os itens da subárvore. Ex.: categoria "EPI" exige CA. |
| isActive | bool | gerado 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
| Campo | Tipo | Regra |
|---|---|---|
| code / name | string(40) / string(80) | Código único por tenant, imutável. |
| dataType | enum | Text | LocalizedText | Integer | Decimal | Measure | Currency | Boolean | Date | List | MultiList | Reference. Imutável após o primeiro valor gravado. |
| uomClassId | Guid? | Obrigatório em Measure. É o que faz 3/8 pol e 9,525 mm serem comparáveis. |
| minValue / maxValue / decimals | decimal? / int | Validação de faixa para tipos numéricos. |
| values | AttributeValue[] | Lista de valores codificados, para List e MultiList. |
| isRestrictive | bool | Lista fechada (só os valores cadastrados) ou sugerida (aceita valor novo, que entra na lista). "Cor da tinta" é fechada; "acabamento" pode ser aberta. |
| isMultiValued | bool | Permite N valores no mesmo atributo. Ex.: certificações: ISO 9001, ISO 14001. |
Vínculo categoria × atributo
| Campo | Tipo | Regra |
|---|---|---|
| categoryNodeId / attributeDefinitionId | Guid | Chave composta. |
| isRequired | bool | Item não é aprovado sem os obrigatórios preenchidos. |
| usedInDescription | bool | Participa da descrição gerada. |
| descriptionOrder | int? | Posição na descrição gerada. Obrigatório quando usedInDescription. |
| displayOrder | int | Ordem no formulário. |
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
| Classe | Base sugerida | Convertível | Observação |
|---|---|---|---|
| Count | UN | sim | DZ, 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. |
| Weight | KG | sim | G, TON. Conversão padrão global. |
| Volume | L | sim | ML, M3. |
| Length | M | sim | MM, CM, KM, POL. |
| Area | M2 | sim | CM2, HA. |
| Time | H | sim | MIN, DIA, MES. Conversão global só onde é inequívoca. |
| Service | VB | não | Verba, 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.
| Entidade | Campos | Invariantes |
|---|---|---|
| ItemSupplier | id, 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. |
| ItemSupplierOffer | id, itemSupplierId, offerKey, revision, supplierPartNumber, supplierAuxPartNumber?, purchaseUomId, numerator, denominator, supplierUomLabel?, moq?, orderIncrement?, standardQuantity?, leadTimeDays?, validFrom, validTo?, contractLineId?, overTolerancePct?, underTolerancePct?, version | Identidade 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. |
| ItemSupplierPrice | id, itemSupplierOfferId, validFrom, validTo?, companyId?, shipToStateCode?, commercialConditionKey, incoterm?, incotermNamedPlace?, priceAmount, currency, priceUomId, priceQty, priceTiers | Preç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. |
| ItemSupplierPriceComponent | id, priceId, kind, amount, currency, basis, includedInPrice, recoverability, sourceRef, observedAt | Custo 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.
Atributo estável do item, necessário antes de comprar — para especificar, comparar, exigir ou bloquear.
Vem no XML da nota. O Nexio armazena como dado observado e usa para conferência. Não se cadastra.
Responsabilidade do ERP fiscal. Guardar aqui produz dado errado e cria duas fontes de verdade.
Mercadorias — quando requiresGoodsTaxCode
| Campo | Balde | Regra no Nexio |
|---|---|---|
| expectedNcm | cadastrar | 8 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. |
| expectedCest | cadastrar | 7 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. |
| gtin | cadastrar | Justificado 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) | esperar | Depende 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, qTrib | não carregar | Campos de emissão. O conceito — unidade de compra × unidade base × fator — está modelado em Unidades; o campo fiscal, não. |
| cProdANP | esperar | Setorial (combustíveis). Vem na nota. |
Serviços — quando requiresServiceTaxCode
| Campo | Balde | Regra no Nexio |
|---|---|---|
| serviceCodeLc116 | cadastrar | Subitem 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. |
| cTribNac | cadastrar | Có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. |
nbsCode | cadastrar | Referência opcional de serviço. Exigibilidade depende do documento, operação e leiaute fiscal vigente; não presumir obrigatoriedade futura sem norma. |
| cTribMun | não carregar | Varia por município do prestador. Não é atributo do item. |
| Alíquotas e regras de retenção | não carregar | Retençã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
| Campo | Balde | Regra no Nexio |
|---|---|---|
| predominantUse | cadastrar | ProductionInput | 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. |
| cClassTrib | não carregar | A 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ções | esperar | Insumo de custo real, vindo da nota. |
| CST/CSOSN, CFOP, alíquotas ICMS/IPI/PIS/COFINS, cBenef, FCI | não carregar | CFOP é 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.
| Caso | Mecanismo | Onde mora |
|---|---|---|
| EPI — Certificado de Aprovação | ItemCompliance | Nº 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) | ItemCompliance | Nº de registro do produto no item. A AFE é do fornecedor, não do item. |
| Registro MAPA (fertilizante, defensivo, veterinário) | ItemCompliance | Nº 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 Civil | flag + habilitação | controlKind 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 — FISPQ | ItemDocument + campos | FISPQ 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 ANATEL | ItemCompliance | Nº do certificado, órgão, validade, arquivo anexo. |
| Exigência de laudo ou certificado na entrega | requiredDocumentKinds | Lista 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
| De | Para | Comando | Regra |
|---|---|---|---|
| — | Draft | POST /items | Quem tem create:catalog. Validação mínima: nome básico, tipo, categoria, unidade base. |
| Draft | PendingApproval | POST /items/{id}/submit | Exige 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. |
| PendingApproval | Approved | tarefa do motor | Decisã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. |
| PendingApproval | Rejected | tarefa do motor | Rejeiçã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. |
| Rejected | Draft | PUT /items/{id} | Editar um item rejeitado o devolve a rascunho. |
| Approved | PendingApproval | PUT /items/{id} → revaluate | Revisto 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.
| Fase | Requisição | Cotação | Pedido | Recebimento |
|---|---|---|---|---|
| New | bloqueado | permitido | bloqueado | bloqueado |
| Active | permitido | permitido | permitido | permitido |
| PhasingOut | aviso | aviso | permitido | permitido |
| Obsolete | bloqueado | bloqueado | bloqueado | permitido |
| Superseded | bloqueado | bloqueado | bloqueado | permitido |
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 match | Ação |
|---|---|---|
| 1 | Exato em (manufacturerName, mpn) normalizado — maiúsculas, sem hífen, sem espaço | bloqueia |
| 2 | Exato em gtin | bloqueia |
| 3 | Exato 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) |
| 4 | attributeSignature — 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 |
| 5 | Similaridade 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 |
| 6 | Re-execução das camadas 4 e 5 na submissão e na aprovação | alerta 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
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.
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.
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.
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.
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.
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.
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.
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 é.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
| Rota | Permissão | Retorno | Erros |
|---|---|---|---|
| GET /catalog/items | read:catalog | Paged de ItemSummaryDto | 400 |
| GET /catalog/items/{id} | read:catalog | ItemDto completo, com atributos, conversões, documentos e compliance | 404 |
| POST /catalog/items | create:catalog | ItemDto em Draft | 400 · 409 · 422 |
| PUT /catalog/items/{id} | update:catalog | ItemDto | 400 · 404 · 409 |
| POST /catalog/items/duplicate-check | read:catalog | Lista de candidatos com score e camada de match. Sem efeito colateral. | 400 |
| POST /catalog/items/{id}/submit | create:catalog | ItemDto em PendingApproval, com approvalRequestId. Abre a instância ITEM no motor | 409 · 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-phase | update:catalog | ItemDto | 400 · 404 · 409 |
| POST /catalog/items/{id}/clone | clone:catalog | Novo item em Draft, sem código interno do original | 404 |
| DELETE /catalog/items/{id} | delete:catalog | Sem conteúdo. Só rascunho nunca submetido | 404 · 409 |
| GET /catalog/items/{id}/audit | read:catalog-audit | Paged de eventos com diff de campos | 404 |
Coleções do item
| Rota | Permissão | Observação |
|---|---|---|
| PUT /catalog/items/{id}/attributes | update:catalog | Reescreve o conjunto inteiro. Recalcula descrição e assinatura. |
| PUT /catalog/items/{id}/uom-conversions | update:catalog | Versiona, 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-intent | update:catalog | Devolve 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}/photos | update:catalog | Confirma 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:catalog | Apaga 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}/documents | update:catalog | Tipo, versão, emissão e validade obrigatórios. |
| PUT /catalog/items/{id}/compliances | update:catalog | Reescreve o conjunto. |
| GET /catalog/items/{id}/suppliers | read:catalog | Paged, filtrável por status. |
| POST /catalog/items/{id}/suppliers | manage:catalog-suppliers | Preço exige manage:catalog-prices adicionalmente. |
| PUT /catalog/items/{id}/suppliers/{supplierItemId} | manage:catalog-suppliers | Concorrência por expectedVersion. |
| DELETE /catalog/items/{id}/suppliers/{supplierItemId} | manage:catalog-suppliers | Apaga 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}/prices | read:catalog | Novo 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}/prices | manage:catalog-prices | Novo 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-prices | Novo 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
| Rota | Permissão | Observação |
|---|---|---|
| GET /catalog/categories | read:catalog | Árvore completa ou subárvore via rootId + levels. Não paginada — é árvore, não lista. |
| POST /catalog/categories | manage:catalog-categories | Valida 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-categories | Renomeia, 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}/parent | manage:catalog-categories | Novo 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}/activate | manage:catalog-categories | Novo 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}/deactivate | manage:catalog-categories | Desativa a subárvore em cascata. Recusa se houver item ativo no nó ou abaixo. expectedVersion obrigatório. |
| PUT /catalog/categories/{id}/attributes | manage:catalog-categories | Vincula atributos e define obrigatoriedade e ordem na descrição. |
| GET /catalog/attribute-definitions | read:catalog | Paged, filtrável por tipo de dado. |
| POST /catalog/attribute-definitions | manage:catalog-attributes | dataType imutável após o primeiro valor gravado. |
| GET /catalog/item-types | read:catalog | Consumido pelo formulário para decidir campos visíveis. |
| POST /catalog/item-types | manage:catalog-item-types | requiresGoodsTaxCode e requiresServiceTaxCode são mutuamente exclusivos. |
| GET /catalog/uoms | read:uoms | Módulo UoM. Filtro por classe, consumido pelo autocomplete. |
| GET /catalog/ncm | read:catalog | Tabela NCM espelhada, busca full-text sobre a descrição concatenada. Global, não por tenant. |
Listagem de itens — filtros e ordenação
| Filtros | Ordenável por | Padrão |
|---|---|---|
| search, categoryNodeId (+includeDescendants), itemTypeId, approvalStatus, lifecyclePhaseId, isPurchasable, supplierId, hasExpiredCompliance, completenessBelow | internalCode, standardDescription, updatedAt, completenessScore | updatedAt 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ódigo | HTTP | Quando |
|---|---|---|
| ITEM_NOT_FOUND | 404 | Item inexistente no tenant |
| ITEM_BASIC_NAME_REQUIRED / _TOO_LONG | 400 | Nome básico vazio ou > 60 |
| ITEM_TYPE_REQUIRED / _INACTIVE | 400 | Tipo ausente ou desativado |
| ITEM_CATEGORY_NOT_POSTABLE | 400 | Categoria informada não aceita item — allowsPosting falso. Substitui ITEM_CATEGORY_NOT_LEAF desde a v3.9 |
| ITEM_CATEGORY_NOT_EFFECTIVE_UNPROCESSABLE | 422 | Categoria fora da vigência |
| ITEM_BASE_UOM_REQUIRED | 400 | Unidade base ausente |
| ITEM_UOM_CLASS_NOT_ALLOWED | 400 | Classe da unidade não permitida pelo tipo |
| ITEM_FIELD_NOT_APPLICABLE | 400 | Campo preenchido que o tipo não usa — mensagem nomeia campo e tipo |
| ITEM_GTIN_INVALID | 400 | Comprimento ou dígito verificador inválido |
| ITEM_NCM_INVALID | 400 | NCM não existe na tabela vigente |
| ITEM_CEST_REQUIRES_NCM | 400 | CEST informado sem NCM |
| ITEM_PRICE_INCOMPLETE | 400 | Preço sem moeda ou sem unidade |
| ITEM_CONVERSION_INVALID | 400 | Numerador ou denominador ≤ 0, ou destino igual à base |
| ITEM_ATTRIBUTE_NOT_IN_SCHEMA | 400 | Atributo não pertence ao esquema da categoria |
| ITEM_ATTRIBUTE_VALUE_INVALID | 400 | Fora da faixa, fora da lista restritiva ou tipo incompatível |
| ITEM_DUPLICATE_CONFLICT | 409 | Match em camada bloqueante — mensagem traz o código interno do item existente |
| ITEM_VERSION_CONFLICT | 409 | Versão defasada no PUT |
| ITEM_BASE_UOM_LOCKED_CONFLICT | 409 | Unidade base já usada em documento |
| ITEM_INVALID_TRANSITION_CONFLICT | 409 | Transição de aprovação inválida a partir do status atual |
| ITEM_HAS_DOCUMENTS_CONFLICT | 409 | Exclusão de item com requisição, cotação, pedido ou nota |
| ITEM_REQUIRED_ATTRIBUTES_MISSING_UNPROCESSABLE | 422 | Submissão sem atributos obrigatórios — lista os códigos faltantes |
| REASON_CODE_REQUIRED_UNPROCESSABLE | 422 | Rejeiçã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_FOUND | 404 | Nó inexistente |
| CATEGORY_MAX_DEPTH_EXCEEDED | 400 | Reservado desde 08/09/2026. Profundidade acima do teto do tenant — só é devolvido quando catalogMaxCategoryDepth tiver valor, e na v1 ele é nulo |
| CATEGORY_CYCLE_DETECTED | 400 | Mover nó para dentro da própria subárvore |
| CATEGORY_SIBLING_NAME_ALREADY_EXISTS | 409 | Nome repetido entre irmãos |
| CATEGORY_NODE_HAS_ITEMS_CONFLICT | 409 | Criar filho sob nó com itens sem a exceção de allowsPosting, ou desativar nó com item ativo no nó ou abaixo |
| CATEGORY_VERSION_CONFLICT | 409 | Versão defasada no PUT, PATCH /parent, PATCH /activate ou DELETE da categoria — ou, num move, o path mudou desde a leitura |
| CATEGORY_ANCESTOR_INACTIVE_UNPROCESSABLE | 422 | Criar, mover ou ativar sob ramo inativo |
| CATEGORY_VALIDITY_OUTSIDE_PARENT_UNPROCESSABLE | 422 | Vigência do filho excede a do pai |
| ATTRIBUTE_DATA_TYPE_LOCKED_CONFLICT | 409 | Alterar tipo de dado com valores já gravados |
| ITEM_NOT_PURCHASABLE_UNPROCESSABLE | 422 | Novo 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_CONFLICT | 409 | Já 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_CONFLICT | 409 | Preço sobrepõe vigência na mesma oferta, empresa, UF e commercialConditionKey. |
ITEM_SUPPLIER_PRICE_INCOTERM_REQUIRED | 400 | Operaçã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_UNPROCESSABLE | 422 | Nenhum preço vigente para oferta e condição selecionadas, após precedência empresa → UF. |
| ITEM_SUPPLIER_PRICE_LOCKED_CONFLICT | 409 | Novo 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_CONFLICT | 409 | Fornecedor bloqueado no módulo Fornecedores |
| ITEM_SUPPLIER_SHIP_TO_STATE_INVALID | 400 | UF fora das 27 unidades federativas |
| ITEM_COMPLIANCE_EXPIRED_CONFLICT | 409 | Compra 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ão | Situação | Cobre |
|---|---|---|
| read:catalog · create:catalog · update:catalog · delete:catalog | existe | CRUD do item |
| import:catalog · export:catalog · clone:catalog | existe | Importação, exportação e clonagem |
| manage:catalog-categories | existe | Árvore de categorias e vínculo de atributos |
| manage:catalog-suppliers | existe | ItemSupplier |
| manage:catalog-prices | existe | Preç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-audit | existe | Histórico por registro |
| approve:catalog-items | novo | Forç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-attributes | novo | Dicionário de atributos |
| manage:catalog-item-types | novo | Tipos 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.
| Coluna | Regra | |
|---|---|---|
| external_code | opc | Chave de upsert. Presente e existente = atualiza; ausente = cria. |
| item_type | obrig | Código do tipo. |
| category_path | obrig | Caminho completo separado por >: FIXADORES > PARAFUSOS > SEXTAVADOS. Nós ausentes são criados quando createMissingCategories: true, respeitando maxDepth. |
| basic_name | obrig | Nome básico. |
| description | cond | Usado apenas quando a categoria não tem esquema de atributos. |
| base_uom | obrig | Símbolo da unidade, validado contra as classes permitidas pelo tipo. |
| attr:{codigo} | cond | Colunas dinâmicas por atributo do esquema. O template baixado já vem com as colunas da categoria escolhida. |
| gtin, mpn, manufacturer | opc | Participam da antiduplicidade. |
| ncm, cest, service_code, ctribnac, nbs | opc | Validados, nunca obrigatórios. |
| reference_price, currency, price_uom | opc | Os três juntos ou nenhum. |
| supplier_code, supplier_part_number, purchase_uom, purchase_factor, price, lead_time, moq | opc | Cria 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 fornece | Como a aprovação usa |
|---|---|
| categoryNodeId + path | A 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. |
| itemTypeId | Dimensã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ítica | requiresContract, criticality e controlKind são candidatos a disparar aprovador funcional obrigatório — Jurídico em contrato, SESMT em produto controlado. |
| referencePrice | Insumo 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. |
| predominantUse | Separa 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
- Estoque — saldo, depósito, ponto de reposição, lote, número de série, validade.
ItemType.controlsStockexiste e é semprefalse. - 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 —
Itemnão aponta paraItem. - 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
ItemSupplierfoi desenhado para receber isso depois. - Tradução de itens — Ariba e Coupa modelam i18n do item como entidade filha. Fica previsto, não implementado.
- 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:
GLAccountespecificada, e a sugestão desce porItem.defaultGlAccountIde depoisCategoryNode.defaultGlAccountIdsubindo 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.