Ponto de partida
Decisões tomadas antes desta especificação
Seis escolhas foram fechadas com o time e são premissa de tudo abaixo. 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. 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).
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 raízes de agregado — duas de configuração (ItemType e ItemLifecyclePhase) e uma, UnitOfMeasure, especificada na Unidade de Medida —, e três filhas da relação ItemSupplier. 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.
ItemSupplierUom, ItemSupplierUomAlias e ItemSupplierPrice.
freightTerms).
uCom) e a qual unidade do Nexio o texto corresponde. Resolve, nunca calcula.
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 (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) | opc | Código legado ou do ERP. Não é único — chave de upsert na importação. |
| itemTypeId | Guid | obrig | Deve referenciar tipo ativo do tenant. |
| gtin | char(14) | opc | GTIN da unidade base do item. Aceita 8, 12, 13 ou 14 dígitos com dígito verificador válido e grava normalizado a 14, com zeros à esquerda. GTIN de embalagem (o GTIN-14 da caixa, que é o que vem no cEAN da nota) mora na conversão ou na unidade de compra que ele identifica (RN-ITM-40). 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, e unidade de embalagem não é base (isPackaging). Imutável depois da primeira linha de documento (RN-ITM-10). |
| 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(21,10) | opc | Benchmark exibido no comparativo de cotações. Exige create:catalog-prices ao informar na criação do item e update:catalog-prices ao alterar. |
| 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. A cópia é regra da spec do Contract, que vem antes do Pedido ou junto com ele: a linha do pedido de item recorrente já nasce com essa origem.
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 | Reservado até a spec do Contract. Hoje só aceita false: gravar true — na criação, na edição, na revisão ou na importação — responde 422 ITEM_REQUIRES_CONTRACT_RESERVED_UNPROCESSABLE. Quando o item passa a exigir contrato vigente para virar pedido, e o que é “contrato vigente”, são regras da spec do Contract, que libera o flag. Essa spec vem antes do Pedido ou junto com ele, então nenhum pedido existe com o flag sem regra. |
| 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: com Strategic ou Bottleneck, vira a flag critical (RN-ITM-43). |
| handlesPersonalData | bool | opc | Default false. Verdadeiro quando o produto ou serviço, em uso, trata dado pessoal de terceiros em nome do tenant — CRM, folha, ponto eletrônico, biometria, BPO de atendimento —, o que faz do fornecedor um operador na LGPD. Não diz nada sobre o cadastro do item, que não guarda dado pessoal. Revisado na aprovação do item. Vira a flag personalData (RN-ITM-43). |
| 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. A única entrada em Approved sem instância é a confirmação da carga inicial do catálogo (RN-ITM-42). |
| origin / initialLoadBatchId | enum / Guid? | gerado | Standard | InitialLoad. InitialLoad só existe em item criado por lote de carga inicial com autoApprove, dentro da janela única do tenant, agrupado pelo lote (RN-ITM-42). |
| 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. Não existe campo de motivo em texto livre com tamanho mínimo: o motivo é codificado, e o comentário é exigido quando o código o exige. |
| 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 | 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 | Cada linha pode levar o gtin daquele nível de embalagem, normalizado a 14 dígitos (RN-ITM-40). 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, 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 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 Raiz é 1; publicado como level = depth - 1, como nas outras duas árvores. |
| version | int | gerado 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 das três árvores; path é guarda de move, não token. |
| sortOrder | int | Ordem de exibição entre irmãos. |
| validFrom / validTo | date? / date? | 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 Nasce true em folha e false quando o nó ganha o primeiro filho. Editável por exceção, nó a nó, por quem tem update: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? | 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. |
| isIdentity | bool | O atributo diz o que o item é e entra na assinatura de antiduplicidade (RN-ITM-31). Independente de isRequired: um atributo opcional pode ser de identidade (revestimento), e um obrigatório pode não ser (cor da embalagem). |
| 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")
A assinatura é o hash de três coisas: o basicName normalizado, o categoryNodeId e os atributos de identidade da categoria (isIdentity), ordenados por código, na forma canônica da RN-ITM-31. 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.isIdentity 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 de identidade 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 | Unidade da classe | Convertível | Observação |
|---|---|---|---|
| Count | UN | sim | PC, PAR, DZ, CT, MI, RS e FL 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, MG, TN. Conversão padrão global. |
| Volume | LT | sim | ML, M3. |
| Length | MT | sim | MM, CM, KM, POL. |
| Area | M2 | sim | Só a própria unidade na semente. |
| Time | HR | sim | MIN converte pela conversão global. DIA, MES e ANO não têm conversão global — "diária" e "mês" são contrato, não física —, e só convertem por item. |
| Service | — | não | VB, SERV, OS, ATV, HH, HM. A classe não tem unidade de referência nem conversão; o item de serviço escolhe uma delas como base. 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 regra lê a classe da unidade base do item, não o tipo: item de base Service não tem conversão alguma; item de serviço com base em HR ou M2 converte dentro da classe, como qualquer outro. E duas propostas na mesma unidade — duas em VB, duas em HH — comparam direto, porque não há conversão a fazer (RN-UOM-17).
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, e ele só vira fator da linha se o comprador o aceitar (RN-ITM-39).
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 linha de documento
O gatilho é um só (RN-ITM-10): a primeira linha de requisição, cotação, pedido, recebimento ou nota que referencie o item, em qualquer estado. 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.
Domínio · ItemSupplier
Item × Fornecedor
Agregado próprio. Coupa (/supplier_items), SAP (purchasing info record), Oracle (Approved Supplier List) e Ariba (o próprio arquivo CIF) são unânimes: essa relação é uma entidade, não uma lista no item.
São duas entidades, não uma. Com a relação de fornecimento e a versão de preço numa linha só, e a vigência na chave, o modelo quebra em quatro pontos: a camada 3 da antiduplicidade bloquearia o código no fornecedor que todo reajuste repete; status, preferência, tolerâncias e reviewByDate seriam reescritos a cada reajuste; "máx. 10 preferidos por item" contaria versões históricas; e o DELETE "só se nunca usado" não teria sujeito. Por isso são ItemSupplier, a relação, e ItemSupplierPrice, a versão de preço. É o info record × condition record da SAP e o ASL × price break da Oracle.
ItemSupplier — a relação de fornecimento
| Campo | Tipo | Regra | |
|---|---|---|---|
| itemId / supplierId | Guid | obrig | Compõem a chave. Fornecedor precisa existir e não estar bloqueado. |
| supplierEstablishmentId | Guid? | opc | Compõe a chave. Nulo = vale para todos os estabelecimentos do fornecedor. É o eixo supplier site da ASL da Oracle, sem obrigar todo cliente a modelar filiais. O nome evita a ambiguidade com o Establishment do lado comprador. |
| supplierPartNumber | string(100) | opc | Código do item no fornecedor. Vai para o pedido e é a chave do matching da nota. Opcional, porque nem todo fornecedor tem código — e campo obrigatório sem valor verdadeiro vira "N/A". Quando informado, é o que a camada 3 da antiduplicidade confere: bloqueia só se o mesmo fornecedor já usa o mesmo código, com o mesmo código auxiliar, para outro item. |
| supplierAuxPartNumber | string(100) | opc | Diferencia variante ou opção de entrega sob o mesmo part number. |
| uoms | ItemSupplierUom[] | obrig | As unidades em que este fornecedor vende o item, cada uma com o seu fator — ao menos uma ativa, exatamente uma padrão. A unidade de compra é do fornecedor, não do item: o A vende em CX de 12; o B, em CX de 24 e em fardo de 240. Ver a tabela abaixo e a RN-ITM-33. |
| uomAliases | ItemSupplierUomAlias[] | opc | Como a nota deste fornecedor escreve a unidade. Máx. 20. Ver a tabela abaixo e a RN-ITM-35. |
| leadTimeDays | int? | opc | Entra no comparativo ao lado do preço — prazo é decisão, não detalhe. |
| status | ItemSupplierStatus | obrig | Active | Blocked, persistido como texto. Nasce Active e só muda por block e unblock (RN-ITM-15) — o PUT da relação não o aceita. Não é rótulo: o efeito está em O que o status da relação libera. Não é o gate de autorização de gasto — quem responde "este fornecedor pode fornecer esta categoria" é a consulta às qualificações vigentes do Fornecedor. |
| blockedAt / blockedBy | datetime? / Guid? | cond | Preenchidos em Blocked, com a quádrupla de motivo do ato (RN-RSN-02 do Motivo); nulos em Active. O bloqueio do fornecedor inteiro não os preenche — ele não reescreve a relação. |
| isPreferred / priority | bool / int | opc | Ordem de sugestão ao abrir RFQ. Máx. 10 preferidos por item, contados em relações — versão de preço não entra na conta. |
| receiptTolerance | obj? | opc | Tolerância de recebimento — quantidade e data — no nível da relação. O objeto, a precedência (estabelecimento → relação → linha, resolvida campo a campo) e a avaliação em unidade base são da Unidade de Medida. |
| reviewByDate | date? | opc | Revisão planejada da relação. Vencida, entra no painel de saúde do catálogo. |
| version | int | obrig | Concorrência otimista, com expectedVersion no PUT (RN-ITM-19). |
O que o status da relação libera
O status é a trava por item da relação. Ele não substitui as outras duas travas, que são do Fornecedor: a tabela “O que cada estado libera” — fornecedor bloqueado, inativo, cadastro fora de Approved — e a qualificação vigente na categoria da linha (RN-FOR-24). A linha de cotação ou de pedido só usa a relação quando as três permitem, lidas quando a linha escolhe a relação e de novo na submissão do documento.
| Status | allowsQuotation | allowsOrder | Entra por | Sai por |
|---|---|---|---|---|
| Active | sim | sim | Criação da relação; unblock | block |
| Blocked | não | não | block, com motivo do uso SupplierBlock | unblock, com motivo do uso SupplierUnblock |
Homologação por item — amostra, PPAP, teste de qualidade — não existe na v1. Quando ganhar consumidor, entra aqui como valor novo do enum, com a sua linha nesta tabela e o ato que a move. Até lá, quem responde “este fornecedor pode fornecer esta categoria” é a qualificação do Fornecedor, e a relação só trava o que é daquele item.
ItemSupplierUom — as unidades de compra da relação
Filha da relação, N por relação: o mesmo fornecedor vende o parafuso em caixa de 12 e em fardo de 240, e troca a caixa de 12 pela de 10 sem deixar de ser o mesmo fornecedor. A unidade de compra não entra na chave da relação porque não muda com quem se compra, e não é versão de preço porque trocar de caixa não muda o preço acordado.
| Campo | Tipo | Regra | |
|---|---|---|---|
| itemSupplierId | Guid | obrig | A relação dona da linha. |
| uomId | Guid | obrig | Unidade ativa, do tenant ou global, de classe que conste em ItemType.allowedUomClasses. Uma linha ativa por unidade na relação — a unicidade vale só entre as linhas ativas. Item cuja base é da classe Service só aceita a própria base, com 1/1 (RN-UOM-08). |
| numerator / denominator | bigint, bigint | obrig | 1 uomId = numerator ÷ denominator × baseUom, inteiros positivos normalizados por MDC. Fornecedor que vende solto tem a base como linha 1/1. Linha usada é imutável (RN-ITM-25): fator diferente vira linha nova, e a usada é desativada. |
| gtin | char(14)? | opc | GTIN desta embalagem, normalizado a 14 dígitos (RN-ITM-40). |
| isDefault | bool | obrig | A unidade sugerida no pedido. Exatamente uma linha ativa padrão por relação. |
| supplierUomLabel | string(40) | opc | Como o fornecedor chama a embalagem na proposta. Só conferência humana — nunca participa de cálculo nem da leitura da nota, que é papel do apelido. |
| moq / orderIncrement / standardQuantity | decimal? | opc | Quantidade mínima, múltiplo e quantidade padrão de pedido, na unidade desta linha — o mínimo de 5 caixas e o mínimo de 1 fardo são regras diferentes. |
| isActive / deactivatedAt | bool / datetime? | gerado | Linha inativa não serve documento novo e continua legível para os documentos que a congelaram. Desativa-se ao ser substituída ou retirada do conjunto, e não se reativa: voltar à caixa antiga é linha nova. |
ItemSupplierUomAlias — a unidade como a nota do fornecedor a escreve
O uCom da NF-e é texto livre do emitente — Cx, CX12, UND — e raramente coincide com o código do Nexio. O apelido é da relação, porque é o jeito deste fornecedor escrever, e serve para resolver qual unidade é, nunca para calcular (RN-ITM-35).
| Campo | Tipo | Regra | |
|---|---|---|---|
| itemSupplierId | Guid | obrig | Compõe a chave. |
| sourceText | string(6) | obrig | Compõe a chave. O texto como vem em uCom ou uTrib — até 6 caracteres, o limite do campo na NF-e —, gravado normalizado: sem espaços, em caixa alta e sem acento. Texto que, normalizado, é código de unidade do Nexio não vira apelido: resolve direto para ela. |
| uomId | Guid | obrig | A unidade do Nexio a que o texto corresponde. Na gravação, precisa ter fator para a base do item — linha ativa da relação, conversão do item ou a própria base. |
ItemSupplierPrice — a versão de preço
Filha da relação, append-only: reajuste é linha nova, e a anterior fecha em validTo. É onde vivem os eixos que fazem o mesmo fornecedor ter mais de um preço para o mesmo item — tempo, empresa compradora, UF de destino e modalidade de frete. Na resolução, empresa e UF formam quatro degraus em ordem total — (empresa, UF) > (empresa, UF nula) > (empresa nula, UF) > (nula, nula) —, e a modalidade de frete bate ou não bate, fora dos degraus (RN-ITM-37).
| Campo | Tipo | Regra | |
|---|---|---|---|
| itemSupplierId | Guid | obrig | Compõe a chave. A relação a que esta versão pertence — e, por ela, o item, o fornecedor e o estabelecimento. |
| validFrom / validTo | date / date? | obrig / opc | validFrom compõe a chave. validTo nulo significa vigente sem prazo. Duas versões da mesma combinação (itemSupplierId, shipToStateCode?, companyId?, freightTerms?) — a chave sem validFrom — não podem cruzar vigência. Reajuste é linha nova, não edição: a anterior fecha em validTo e o histórico de preço fica de pé. Preço vencido não entra em comparativo sem aviso. |
| shipToStateCode | char(2)? | opc | Compõe a chave. Nulo = vale para qualquer destino. UF de entrega, não ponto de entrega: o que muda preço é frete e tributação, e dois pontos na mesma cidade não têm preço diferente — chavear pelo ponto faria o catálogo crescer sem teto e ninguém o manteria. A chave de busca é o deliveryStateCode congelado na linha do documento, então resolver preço não consulta o cadastro do ponto de entrega. Ver Local de Entrega. |
| companyId | Guid? | opc | Compõe a chave. Empresa compradora a que este preço se aplica; nulo = vale para todas. Condição comercial é negociada por CNPJ comprador — regime tributário, ICMS/ST e contrato mudam com ele —, e por isso a empresa está na mesma chave que a UF. A v1 pode gravar sempre nulo: a coluna é gancho barato hoje e migração de chave primária em agregado povoado depois. Ver Empresa. |
| freightTerms | enum? | cond | Compõe a chave. A modalidade de frete, no vocabulário do modFrete da NF-e — é o que diz se o preço cobre o transporte, e é contra ele que o modFrete da nota é conferido. Obrigatório quando o tipo do item tem requiresGoodsTaxCode; nulo em todos os demais, porque serviço não tem transporte a declarar. Preço de mercadoria sem modalidade é ininterpretável — a mesma falha que a RN-ITM-18 corrige para a unidade. Na chave porque o preço com frete e o preço sem frete do mesmo fornecedor, para a mesma UF e no mesmo período, precisam coexistir: é a comparação que o comparativo existe para fazer. Valores na tabela abaixo; RN-ITM-27. |
| incoterm | string(3)? | opc | Fora da chave, e só em importação: aceito apenas quando o fornecedor é Foreign (ver Fornecedor). Uma das onze regras do Incoterms 2020 — EXW, FCA, CPT, CIP, DAP, DPU, DDP, FAS, FOB, CFR, CIF. Diz o que o freightTerms não diz — seguro, desembaraço, ponto de transferência de risco — e precisa concordar com ele sobre quem contrata o transporte principal (RN-ITM-27). Fica fora da chave porque a comparação que precisa coexistir no catálogo, com frete × sem frete, o freightTerms já separa. |
| incotermNamedPlace | string(160) | cond | Obrigatório quando há incoterm, e proibido sem ele — toda regra do Incoterms 2020 nomeia um lugar. É o "Xangai" de FOB Xangai: o lugar que a regra nomeia, que nunca é um ponto de entrega nosso. |
| priceAmount / currency | decimal(21,10) / char(3) | obrig | Moeda ativa no tenant. Dez casas decimais — ver Unidade de Medida. |
| priceUomId / priceQty | Guid / decimal(19,6) | obrig | Pode diferir da unidade de compra — compra-se em SC e cobra-se por TN. priceQty é a quantidade de precificação (preço "por 1000"). A unidade de preço precisa ter conversão conhecida para a base, e o fator dessa conversão fica congelado na própria versão e é copiado para a linha do documento (RN-ITM-29, RN-ITM-38). |
| priceConvNum / priceConvDen | bigint, bigint | gerado | Fator da unidade base para a unidade de preço, resolvido pela hierarquia na gravação da versão e imutável nela. Unidade de preço igual à base grava 1/1. É o que diz, para sempre, "este preço foi acertado por caixa de 12" (RN-ITM-38). |
| priceItemSupplierUomId | Guid? | cond | A linha de ItemSupplierUom de onde veio o fator, obrigatória quando a unidade de preço resolve pela unidade de compra do fornecedor; nula quando resolve pela conversão do item, pela global ou é a base. Linha desativada tira a versão da sugestão de preço até existir versão nova (RN-ITM-38). |
| priceTiers | tier[] | opc | Faixas por quantidade: (minQty, priceAmount), com minQty na unidade de preço da versão e priceAmount no mesmo priceQty. Máx. 10, minQty crescente e único. Vale a faixa de maior minQty ≤ quantidade da linha na unidade de preço, e o preço dela vale para a linha inteira — não é escalonado (RN-ITM-37). Pertencem à versão, não à relação: faixa é preço. |
freightTerms — a modalidade de frete, no vocabulário do modFrete
| Valor | modFrete | Quem contrata o transporte | No comparativo, o preço conta como |
|---|---|---|---|
| ContractedBySender | 0 | O remetente — o fornecedor — contrata o transportador. É o "CIF" do mercado brasileiro. | com transporte |
| ContractedByRecipient | 1 | O destinatário — o comprador — contrata o transportador. É o "FOB" do mercado brasileiro. | sem transporte |
| ContractedByThirdParty | 2 | Um terceiro, nem remetente nem destinatário, contrata o transporte. | sem transporte |
| OwnTransportBySender | 3 | O remetente entrega com transporte próprio. | com transporte |
| OwnTransportByRecipient | 4 | O destinatário retira com transporte próprio. | sem transporte |
| NoTransport | 9 | Não há ocorrência de transporte. | sem transporte |
Remetente e destinatário são os papéis de quem envia e de quem recebe a mercadoria, não de quem emite a nota: numa nota de entrada emitida pelo próprio comprador, o remetente continua sendo o fornecedor. O valor persiste como texto, por convenção do Nexio; o código numérico é tradução, na conferência da nota e na integração. A última coluna é regra declarada, não inferência: o comparativo trata como preço com transporte só os valores em que é o remetente quem contrata ou faz o transporte (0 e 3); nos demais — inclusive o terceiro (2), cujo custo o Nexio não conhece — o preço conta como sem transporte e o comparativo sinaliza que o frete fica por fora. A conferência modFrete × freightTerms não vale para a NF-e de entrada de importação: nela, emitida pelo próprio importador, o modFrete descreve o trecho nacional, não o transporte que o preço do fornecedor estrangeiro cobre — para esse, vale o incoterm, e a nota não é conferida contra a modalidade da versão de preço. Na importação, o freightTerms diz quem contrata o transporte principal que o preço cobre — FOB Xangai é ContractedByRecipient, CIF Santos é ContractedBySender — e o incoterm detalha o resto.
As duas chaves. A relação tem três partes — (itemId, supplierId, supplierEstablishmentId?). A versão de preço tem a chave da relação mais quatro: (itemSupplierId, validFrom, shipToStateCode?, companyId?, freightTerms?). Item, fornecedor e estabelecimento ela herda da relação; tempo, destino, empresa compradora e a modalidade de frete são dela. As unidades de compra e os apelidos de unidade são filhas da relação e não entram em chave nenhuma de preço: a embalagem não muda com quem se compra nem quanto se acordou pagar. A UF fica aqui, e não na relação — e é o que torna verdadeira a frase que esta página promete: destino e estabelecimento são dimensão de preço, sem serem também dimensão de status. Com a UF na chave da relação ela seria as duas coisas: cada linha carregaria o seu status, as suas unidades de compra e as suas tolerâncias, e um preço nacional com uma exceção de SP produziria duas relações para um único relacionamento, sem resposta para qual delas responde pelo fornecedor. Mais: a FK da linha do documento aponta para a relação, e com a UF nela mudar o endereço de entrega de uma requisição mudaria de relacionamento de fornecimento — mudar onde se recebe muda o preço, nunca o fornecedor. O custo das colunas anuláveis continua e precisa ser dito: dois nulos não colidem em vários bancos, e uma unicidade comum deixaria passar duas linhas "vale para tudo". O requisito é que a unicidade valha tratando nulos como iguais — índices parciais, um por combinação de nulo, ou um índice único que trate nulos como iguais onde o banco oferecer. A verificação de cruzamento de vigência acontece na aplicação, na mesma transação, em qualquer banco. É a mesma forma que o CostCenterAssignment e o BudgetPolicy já usam para período.
Qual relação vale, e depois qual preço. São dois passos, e não um — foi o que a inversão da UF deixou legível. Primeiro escolhe-se a relação: estabelecimento exato → estabelecimento nulo. Só então, dentro dela, a versão de preço vigente na data de submissão, descendo o degrau de cada dimensão anulável — UF exata → UF nula e empresa do documento → empresa nula — só entre as versões da modalidade de frete pedida. freightTerms não é degrau: bate ou não bate, e nunca se empresta o preço com frete para quem pediu sem frete. Quem resolve preço sem informar a modalidade — a requisição, que não negocia frete — recebe a versão vencedora de cada modalidade presente, e a linha só ganha preço sugerido quando houver uma só; havendo mais de uma, as candidatas são exibidas e o sistema não escolhe. O pedido sempre informa a modalidade. Cada dimensão anulável é um degrau, e a ordem é total (RN-ITM-37): (empresa, UF) → (empresa, UF nula) → (empresa nula, UF) → (empresa nula, UF nula) — a empresa compradora vem antes da UF, porque condição negociada pelo CNPJ comprador vence tabela regional. É a mesma forma da hierarquia de conversão da unidade de medida (ItemSupplier > Item > global > erro). Não existe degrau heurístico: UF bate ou não bate, e nunca se empresta o preço de um destino parecido.
Precedência de valores na linha do pedido: ContractItem vigente → relação (versão de preço e unidade de compra) → Item. O contrato não é campo da relação: é o futuro ContractItem que aponta para o item ou para a relação, com empresa e vigência próprias (RN-ITM-34). O item só fornece o que ninguém mais forneceu (unidade base, descrição, atributos). Preço e prazo vindos do item são sempre referência, nunca compromisso.
O que o preço inclui, e o que fica para depois. A modalidade de frete responde a metade que dói — "um inclui frete e outro não" — e responde de forma comparável, porque é vocabulário fechado e não texto livre. A outra metade, decompor o preço em componentes de custo (mercadoria, frete, seguro, embalagem especial, ferramental, tributo não recuperável), fica como gancho nomeado: ItemSupplierPriceComponent, filha da versão de preço, não especificada nesta versão. O gatilho está escrito: ela nasce quando o comparativo precisar somar componente que a modalidade de frete não cobre — hoje, o único consumidor seria a própria tela de cadastro, e um campo que ninguém lê é campo que ninguém preenche direito. Ver Fora de escopo.
Bloquear o fornecedor no Fornecedor não reescreve os ItemSupplier dele: o efeito é lido na hora, pelo eixo Bloqueio de lá (RN-ITM-15) — sem apagar o registro, e sem tocar nas versões de preço, que são histórico. Desativação destrutiva é o erro documentado da Coupa, onde Active = No destrói os supplier items.
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 | esperar | Não vivem no item: são observados na nota e guardados na linha dela, como o emitente os declarou. uTrib resolve pela mesma regra do uCom — código exato, depois apelido da relação (RN-ITM-35) — e qTrib é conferido na unidade base; cEANTrib é conferido contra os GTIN do item, das conversões e das unidades de compra (RN-ITM-40). O conceito — unidade de compra × unidade base × fator — está modelado em Unidades e na ItemSupplierUom. |
| 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 | 9 dígitos, formato 1.XXXX.XX.XX. Opcional hoje, com tendência clara de obrigatoriedade conforme IBS/CBS amadurece. Campo preparado agora, exigido depois. |
| 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. |
Por que não existe campo "retenções" no item. IRRF, INSS, CSRF e ISS retido dependem de: (a) tipo de serviço — atributo do item; (b) natureza jurídica e regime do prestador — atributo do fornecedor; (c) forma de execução — atributo do contrato ou do pedido: a mesma "manutenção predial" dispara INSS de 11% com equipe alocada e pode não disparar por empreitada de resultado; (d) município e regra de local da prestação — atributo da operação.
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. |
Durante 2026 as notas chegam com dados de IBS/CBS incompletos ou ausentes e ainda assim autorizadas — o Ato Técnico Conjunto RFB/CGIBS nº 1/2026 adiou as validações, não a obrigação. O parser de nota não pode tratar ausência como erro.
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 (regulated, personalData e critical, RN-ITM-43) 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. |
| 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 | Approved · revisão pendente | PUT /items/{id} | Alteração de campo material — categoria, tipo, unidade base (enquanto a RN-ITM-10 permitir), atributos de identidade, gtin, manufacturerName, mpn — num item aprovado não muda o item: grava a revisão pendente (RN-ITM-41). O item continua Approved e comprável com os valores aprovados. Campo não material grava direto. |
| Approved · revisão pendente | Approved | POST /items/{id}/revision/submit → tarefa do motor | Abre instância nova do processo ITEM sobre a revisão, com antiduplicidade refeita nos valores revisados. Aprovada, o contrato de desfecho aplica a revisão ao item na mesma transação; rejeitada ou descartada, a revisão some e o item segue como estava. A instância que aprovou o cadastro não é reaberta. 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. 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 normalizado a 14 dígitos, em qualquer nível — item, conversão do item ou unidade de compra (RN-ITM-40) | bloqueia quando aponta para outro item |
| 3 | Exato em (supplierId, supplierPartNumber, supplierAuxPartNumber?) normalizados — maiúsculas, sem espaço, hífen, ponto ou barra —, só quando supplierPartNumber está preenchido | bloqueia na relação ItemSupplier e só quando a trinca já aponta para outro item. O mesmo código no mesmo item, em outro estabelecimento do fornecedor, é a mesma coisa comprada de outro CNPJ e passa; código igual com auxiliar diferente é variante e passa (RN-ITM-26) |
| 4 | attributeSignature — hash de basicName + categoryNodeId + atributos de identidade, na forma canônica (RN-ITM-31); e crossCategorySignature, o mesmo hash sem a categoria | bloqueia na mesma categoria; alerta quando a assinatura sem categoria casa com item de outra categoria — é a compra do mesmo produto por três departamentos. Não se aplica em categoria sem atributo de identidade, 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 na transação do forçar como ato de controle: autor, item, candidato e motivo, em registro isolado por tenant gravado na mesma transação, requisito que a demanda de plataforma cobra do registro de auditoria. 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. Ver RN-ITM-32.
A unidade base é imutável depois da primeira linha de documento, e nunca é embalagem
Um gatilho só, o mesmo na Unidade de Medida (RN-UOM-15): existindo qualquer linha de requisição, cotação, pedido, recebimento ou nota que referencie o item, em qualquer estado — rascunho incluído —, baseUomId não muda. Alteração responde 409 ITEM_BASE_UOM_LOCKED_CONFLICT e sugere criar item sucessor. E a base não pode ser unidade de embalagem (isPackaging): a base é a única constante da cadeia, e o conteúdo de uma caixa não é constante — 400 ITEM_BASE_UOM_PACKAGING_NOT_ALLOWED.
Item só classifica em nó ativo, vigente e com allowsPosting
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
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.
A relação se bloqueia por ato próprio, e o bloqueio do fornecedor não a reescreve
ItemSupplier.status é Active | Blocked. Nasce Active e só muda por POST …/block e …/unblock, com update:catalog-suppliers, expectedVersion, motivo codificado dos usos SupplierBlock e SupplierUnblock e trilha item-supplier.blocked e item-supplier.unblocked. Repetir o estado atual é ITEM_SUPPLIER_STATUS_UNCHANGED_CONFLICT. Bloquear preserva preço, histórico, prioridade e unidades, e não mexe em documento já submetido: vale para linha nova e para a próxima submissão.
O bloqueio do fornecedor inteiro não muda a relação. O efeito é derivado na leitura: a relação só entra em cotação ou pedido quando o status dela permite e a tabela “O que cada estado libera” do Fornecedor permite, as duas lidas quando a linha escolhe a relação e de novo na submissão. Não há evento a esperar nem status anterior a guardar: desbloqueado o fornecedor, cada relação volta a valer como estava, e a que foi bloqueada por ato próprio continua Blocked.
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 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.
Destino é dimensão de preço, e a dimensão é a UF
shipToStateCode compõe a chave da versão de preço ItemSupplierPrice, não a da relação — destino é eixo de frete e tributação, e frete e tributação são preço, não relacionamento. A resolução na linha do documento usa o deliveryStateCode congelado no snapshot de entrega e desce os degraus do mais específico ao mais genérico, sem nunca aproximar: não existindo linha para a UF nem linha sem destino, o preço não é sugerido. Preço por ponto de entrega específico não vive aqui — é negociação, e mora no ContractItem (RN-ITM-34).
A categoria tem duas portas de status, com cascata assimétrica
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
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.
Preço de fornecedor tem vigência, e a vigência está na chave
A vigência está na chave da versão de preço: a unicidade do ItemSupplierPrice é (itemSupplierId, validFrom, shipToStateCode?, companyId?, freightTerms?), tratando nulos como iguais. A regra de não sobreposição é outra e mais forte: duas versões da mesma combinação (itemSupplierId, shipToStateCode?, companyId?, freightTerms?) — sem validFrom, porque com ele toda versão seria uma combinação própria e a regra nunca dispararia — não podem ter vigências que se cruzem. Verificado na aplicação, na mesma transação da gravação e sob trava da relação, com guarda no banco quando ele existir; cruzamento responde 409 ITEM_SUPPLIER_PRICE_DUPLICATED_CONFLICT. Reajustar preço é gravar linha nova e fechar a anterior, nunca editar a vigente: a linha vigente é o que o comparativo lê, e as fechadas respondem qual preço estava acordado em março. Quanto se pagou em março é outra pergunta, e não se responde pelo cadastro: é projeção das linhas de pedido e de nota, que congelam a versão usada e o valor efetivamente praticado. A resolução de preço num documento usa a linha vigente na data de submissão dele, e o resultado vai congelado para a linha, como manda a Regra 1 do mapa de domínio.
Fator em uso nunca é reescrito; embalagem nova é linha nova
numerator e denominator já usados em cotação, pedido, recebimento ou nota não mudam no registro. Na unidade de compra do fornecedor (ItemSupplierUom), a linha usada fica imutável: quando o fornecedor passa a entregar 10 onde entregava 12, grava-se linha nova — pode ser o mesmo código CX — e a usada é desativada na mesma transação. Na conversão do item (ItemUomConversion), fator novo para unidade já usada versiona: a vigente fecha em validTo e a nova abre em validFrom. É uma regra só, em duas formas porque uma entidade tem vigência e a outra não — a RN-UOM-09 —, e o PUT do conjunto aplica as duas sem responder 409 por fator em uso. O documento congela o registro e o fator que usou (itemSupplierUomId, convNum/convDen, RN-UOM-10), então nem documento emitido nem documento em aberto muda de significado; o que se protege aqui é o cadastro: com o fator editado no lugar, CX valeria 12 até ontem e 10 hoje, e nenhuma consulta saberia qual valia. Registro nunca usado corrige-se no lugar.
Relação de fornecimento e versão de preço são entidades distintas
ItemSupplier é a relação, com chave de três partes (itemId, supplierId, supplierEstablishmentId?) — a UF está na chave do preço, para não ser também dimensão de status —, e é dona de status, isPreferred/priority, tolerâncias, reviewByDate e código no fornecedor. Tem três filhas: as unidades de compra (ItemSupplierUom, RN-ITM-33), os apelidos de unidade da nota (ItemSupplierUomAlias, RN-ITM-35) e a versão de preço ItemSupplierPrice, append-only. Quatro consequências, e cada uma fechava um furo: a camada 3 da antiduplicidade bloqueia na relação, e por isso reajustar preço deixa de colidir com ela; "máx. 10 preferidos por item" conta relações, não versões históricas; o DELETE "só se nunca usado" volta a ser decidível, porque existe um registro que é "a relação"; e a FK da linha do documento aponta para a relação e congela, ao lado, a versão de preço resolvida na submissão e a unidade de compra usada — a linha guarda os três identificadores, e o comparativo sabe exatamente qual tabela pagou aquele preço.
O preço de mercadoria declara a modalidade de frete, e ela está na chave
freightTerms é obrigatório em toda versão de preço de item cujo tipo tem requiresGoodsTaxCode, nulo em todas as demais, e compõe a chave. O vocabulário é o do modFrete da NF-e — ContractedBySender (0), ContractedByRecipient (1), ContractedByThirdParty (2), OwnTransportBySender (3), OwnTransportByRecipient (4), NoTransport (9) —, persistido como texto; o comparativo conta como preço com transporte só 0 e 3. O modFrete da nota é conferido contra ele, exceto na NF-e de entrada de importação, em que o modFrete é do trecho nacional. Obrigatório porque preço de mercadoria que não diz se cobre o transporte é ininterpretável; na chave porque o preço com frete e o preço sem frete do mesmo fornecedor, para a mesma UF e no mesmo período, precisam coexistir. Não é degrau de resolução: bate ou não bate, e quem resolve sem informar a modalidade só recebe preço sugerido quando existe uma única. incoterm fica fora da chave e é opcional, aceito só com fornecedor de entityType = Foreign; quando presente, exige incotermNamedPlace e um freightTerms do lado que a regra atribui ao transporte principal — EXW, FCA, FAS e FOB com ContractedByRecipient ou OwnTransportByRecipient; CPT, CIP, CFR, CIF, DAP, DPU e DDP com ContractedBySender ou OwnTransportBySender. Recusas, na gravação da versão: 400 ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_REQUIRED, 400 ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_NOT_APPLICABLE, 400 ITEM_SUPPLIER_PRICE_INCOTERM_NOT_APPLICABLE, 400 ITEM_SUPPLIER_PRICE_INCOTERM_FREIGHT_MISMATCH. Trocar o tipo do item entre mercadoria e não mercadoria (RN-ITM-06) não reescreve versões: as que ficaram incoerentes com o tipo deixam de ser resolvidas, aparecem no painel de saúde do catálogo, e o cadastro grava versão nova. Decompor o preço em componentes de custo fica como gancho nomeado (ItemSupplierPriceComponent), não especificado enquanto a modalidade de frete der conta.
O preço tem eixo de empresa compradora, anulável
companyId é anulável e compõe a chave da versão de preço; nulo vale para todas as empresas do tenant. A v1 pode gravar sempre nulo — a coluna é gancho, e o que ela evita é migração de chave primária em agregado povoado. A resolução desce o degrau: preço da empresa do documento, depois preço sem empresa; nunca o preço de outra empresa. O catálogo continua sendo vocabulário do tenant — item, tipo, categoria e unidade não ganham empresa; o que ganha é a condição comercial, que é negociada por CNPJ comprador e muda com regime tributário, ICMS/ST e contrato. Ver Empresa e Estabelecimento.
A linha congela também o fator da unidade de preço
Ao lado de convNum/convDen — o fator da unidade pedida para a base —, toda linha de documento congela priceConvNum/priceConvDen, o fator da unidade base para a unidade de preço. Sem eles, o caso de primeira classe ItemSupplierUom.uomId = SC com priceUom = TN obrigava a resolver a conversão no cadastro no momento do cálculo — e aí uma conversão nova, ou uma linha nova de unidade de compra, mudava o total de uma linha antiga. É exatamente a divergência de centavos que produz as rejeições 629 e 630 da SEFAZ, e a conferência a três existe para não tê-la. O par é copiado da versão de preço resolvida, que o congelou na gravação (RN-ITM-38). Quando priceUomId é a unidade base, o par é 1/1; quando é a própria unidade pedida, é o inverso de convNum/convDen — e em qualquer caso é gravado: snapshot não tem exceção. Ver RN-UOM-10.
Só vira linha de documento o item aprovado, na fase certa e comprável
Os três eixos são ortogonais no cadastro e conjuntivos na compra: para o item entrar como linha de requisição, cotação ou pedido, é preciso approvalStatus = Approved, fase cuja matriz permite ou avisa aquele processo, e isPurchasable verdadeiro. Faltando qualquer um: 422 ITEM_NOT_PURCHASABLE_UNPROCESSABLE, com a mensagem nomeando qual dos três recusou. Sem esta regra a curadoria de catálogo não bloqueava nada — um item rejeitado em fase Active era pedível — e um processo de aprovação que não impede nada é fila que se aprende a ignorar.
A assinatura lê nome, categoria e atributos de identidade, numa forma canônica declarada
attributeSignature = sha256(basicName | categoryNodeId | atributos de identidade) e crossCategorySignature = sha256(basicName | atributos de identidade). Entram os atributos com isIdentity — obrigatórios ou não —, ordenados por código; atributo de identidade vazio entra como código=∅, para que o item com revestimento e o item sem ele não colidam. Forma canônica, a mesma em toda gravação: texto e basicName em decomposição Unicode sem acentos, maiúsculas, espaços internos colapsados em um e sem espaços nas pontas; número em decimal com ponto, sem separador de milhar e sem zeros à direita; Measure convertido para a unidade da classe por par inteiro e escrito com até 10 casas, ROUND_HALF_UP, seguido de : e do código da unidade (3/8 pol e 9,525 mm dão 0.009525:MT); booleano TRUE/FALSE; lista pelo código da opção; multivalorado com os códigos ordenados e separados por vírgula. A camada 4 bloqueia quando attributeSignature casa na mesma categoria e alerta quando só crossCategorySignature casa, com item de outra categoria. Alternar CategoryAttribute.isIdentity, ou vincular e desvincular atributo de identidade, dispara recálculo assíncrono das duas assinaturas do nó e da subárvore, com a camada 4 suspensa naquele ramo até terminar. Categoria sem atributo de identidade não tem assinatura — as duas são nulas, a camada 4 é pulada e as outras cinco continuam valendo.
Conversão interclasse é do item, e o item precisa declarar a equivalência
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.
A unidade de compra é filha da relação, N por relação, e só a linha ativa serve documento novo
ItemSupplierUom guarda as unidades em que o fornecedor vende o item, cada uma com fator próprio para a base. A relação tem ao menos uma linha ativa, uma ativa por unidade e exatamente uma padrão; a classe de cada unidade precisa constar em ItemType.allowedUomClasses, e item de base Service só aceita a própria base com 1/1. As linhas mudam só pelo PUT do conjunto, com o expectedVersion da relação: fator diferente para unidade já usada desativa a linha usada e cria outra, na mesma transação (RN-UOM-09); unidade retirada do conjunto é desativada se já usada e apagada se nunca usada; linha desativada não se reativa. Documento novo só usa linha ativa, e a linha do documento congela itemSupplierUomId ao lado de convNum/convDen. Pedido em aberto que congelou linha desativada continua valendo com o fator que congelou — é ele que o recebimento sinaliza quando a embalagem muda. Recusas: 400 ITEM_SUPPLIER_UOM_SET_INVALID (conjunto sem linha, unidade repetida ou padrão diferente de uma), 400 ITEM_UOM_CLASS_NOT_ALLOWED, 400 ITEM_SUPPLIER_UOM_SERVICE_BASE_ONLY e 409 ITEM_VERSION_CONFLICT.
O contrato aponta para o item ou para a relação, nunca o contrário
ItemSupplier não carrega contrato. Um mesmo par item × fornecedor é coberto por vários contratos — renovações, um por empresa compradora, contrato guarda-chuva —, e a relação não tem empresa nem vigência para distingui-los: com o contrato nela, renovar reescreveria a referência e apagaria o histórico. O futuro ContractItem, na especificação do Contrato, referencia itemId ou itemSupplierId, com companyId anulável e vigência próprios, e é ele que sobrepõe preço, prazo e múltiplo na linha do pedido quando estiver vigente para a empresa do documento. Até ele existir, a precedência é relação (versão de preço e unidade de compra) → Item, e nenhum campo deste módulo espera pelo contrato.
A unidade escrita na nota resolve por código exato ou apelido da relação, nunca por semelhança
A conferência da nota resolve o texto de uCom — e o de uTrib — em dois passos, sobre o texto normalizado (sem espaços, em caixa alta, sem acento): código exato de unidade ativa do tenant ou global; senão, apelido da relação da linha (ItemSupplierUomAlias). Não resolvendo, é exceção de conferência — nunca unidade parecida, nunca classe genérica, nunca 1:1. O apelido diz qual unidade é e nunca carrega fator: a quantidade na base vem do snapshot da linha do pedido quando a unidade resolvida é a pedida, e da hierarquia de conversão (RN-UOM-06) quando não é. Três guardas, porque apelido errado muda a leitura de quantidade: texto que normalizado já é código de unidade não aceita apelido — UN nunca significa CX (422 ITEM_SUPPLIER_UOM_ALIAS_IS_UOM_CODE_UNPROCESSABLE); o destino precisa ter fator para a base do item na gravação (422 ITEM_SUPPLIER_UOM_ALIAS_TARGET_UNRESOLVABLE_UNPROCESSABLE); e mudar apelido não refaz conferência concluída — vale para as notas conferidas depois. Máximo de 20 apelidos por relação (400 ITEM_SUPPLIER_UOM_ALIAS_LIMIT_EXCEEDED); toda mudança entra na auditoria da relação (RN-ITM-20). uTrib, qTrib e cEANTrib observados ficam na linha da nota, como o emitente os declarou; o catálogo não os copia.
Linha não catalogada é declarada: sem item, com categoria, unidade e destinação
Linha de requisição, cotação ou pedido pode ter itemId nulo — a compra do que ainda não está no catálogo. Não é item provisório nem rascunho, e não passa pela curadoria: é a própria linha. São obrigatórios nela description (texto livre, até 500 caracteres), categoryNodeId em nó ativo, vigente e com allowsPosting (as condições da RN-ITM-11), uomId de unidade ativa e predominantUse. A unidade informada é também a base da linha: convNum/convDen gravam 1/1 e nenhuma conversão é resolvida. itemSupplierId, itemSupplierPriceId e itemSupplierUomId são nulos, o preço é o informado na linha, e a RN-ITM-30 não se aplica, porque não há item cujos eixos consultar. A conta contábil sugerida vem do defaultGlAccountId do nó, como em qualquer linha. Produto regulado não entra por aqui: categoria cujo nó declara ou herda requiredComplianceKinds recusa linha não catalogada, porque a validade do registro regulatório (RN-ITM-16) só existe no item. O motor de aprovação recebe isUncatalogued = true, e é com essa marca que o tenant exige aprovação para compra fora do catálogo, sem código. Recusas: 400 ITEM_UNCATALOGUED_LINE_INCOMPLETE, nomeando o campo, e 422 ITEM_UNCATALOGUED_LINE_CATEGORY_UNPROCESSABLE, nomeando o motivo. Cadastrar depois um item para aquilo não altera a linha emitida, que continua sem itemId.
A resolução de preço tem ordem total, e a faixa é pela quantidade na unidade de preço
Escolhida a relação (estabelecimento exato → estabelecimento nulo), a versão de preço é procurada entre as vigentes na data de submissão e da modalidade de frete pedida, nesta ordem, e a primeira que existir vence: (1) empresa do documento e UF de destino da linha; (2) empresa do documento e UF nula; (3) empresa nula e UF de destino; (4) empresa nula e UF nula. A empresa vem antes da UF porque condição negociada pelo CNPJ comprador vence tabela regional. Como a não sobreposição (RN-ITM-24) garante no máximo uma vigente por degrau, a ordem é total e o resultado é único. Nada é emprestado de outra empresa, de outra UF nem de outra modalidade de frete; sem versão em nenhum degrau, 422 ITEM_SUPPLIER_PRICE_NOT_EFFECTIVE_UNPROCESSABLE. Na versão vencedora, a faixa aplicada é a de maior minQty menor ou igual à quantidade da linha convertida para a unidade de preço (priceUomId, com o fator congelado da versão); sem faixa que caiba, vale priceAmount. O preço da faixa vale para a linha inteira — não é escalonado —, e a linha congela a versão e a faixa usadas.
A versão de preço congela o fator da sua unidade, e embalagem que muda tira o preço da sugestão
Na gravação, a versão resolve a unidade de preço pela hierarquia de conversão e guarda priceConvNum/priceConvDen — da base para a unidade de preço — e, quando o fator veio da unidade de compra do fornecedor, a linha de origem em priceItemSupplierUomId. Os dois são imutáveis. Na resolução (RN-ITM-37), uma versão só é sugerível se a sua unidade de preço ainda resolve, hoje, para o mesmo fator e, quando há priceItemSupplierUomId, se essa linha está ativa. Senão, a versão sai da sugestão, aparece no painel de saúde do catálogo, e o degrau segue como se ela não existisse, até alguém gravar versão nova. É o que impede o preço acertado por caixa de 12 de valer para a caixa de 10: 7 caixas a R$ 37,00 eram 84 UN a R$ 3,0833 cada; com a caixa de 10, as mesmas 7 caixas pelos mesmos R$ 259,00 são 70 UN a R$ 3,70 — 20% mais caro, sem nenhum número mudar na tela. A linha de documento já emitida continua com o fator que congelou (RN-ITM-29).
Linha de item catalogado pode não ter relação, e o fator declarado só vale se aceito na linha
Linha de cotação, pedido ou recebimento de item catalogado pode ter itemSupplierId nulo — fornecedor que ainda não tem relação com o item, a compra spot. A RN-ITM-30 continua valendo; não há preço sugerido, e o preço é o cotado ou o informado. A unidade converte pelos níveis 2 e 3 da hierarquia. Quando o fornecedor cota numa unidade sem conversão — tipicamente a caixa dele —, o fator que ele declara na cotação (declaredFactor) só vale se o comprador o aceitar na linha, em ato explícito que grava quem aceitou e quando; o fator aceito vai congelado em convNum/convDen com factorSource = DeclaredAccepted. Se o item já tem conversão para aquela unidade e o fator declarado difere, o aceite exige justificativa em texto. O fator aceito nunca vira cadastro — nem conversão do item, nem unidade de compra — e cada linha grava de onde veio o seu fator: factorSource = SupplierUom | ItemConversion | StandardConversion | DeclaredAccepted | Identity. Sem fator resolvido nem aceito, 422 UOM_CONVERSION_NOT_CONFIGURED_UNPROCESSABLE. O recebimento continua conferindo em unidade base, e é a tolerância que pega fator declarado errado.
GTIN identifica um nível de embalagem, e é normalizado a 14 dígitos
O gtin do item é o da unidade base. A caixa tem GTIN próprio — o GTIN-14 que o fornecedor põe no cEAN da nota —, e ele mora no nível que identifica: ItemUomConversion.gtin ou ItemSupplierUom.gtin (char(14)?). Todo GTIN é validado pelo dígito verificador, aceito com 8, 12, 13 ou 14 dígitos e gravado com 14, zeros à esquerda; o texto SEM GTIN da nota é tratado como ausente. Um GTIN normalizado identifica um item e um fator para a base no tenant: repetido no mesmo item com o mesmo fator — a mesma caixa vendida por dois fornecedores — é aceito; apontando para outro item ou para outro fator, 409 ITEM_GTIN_ALREADY_EXISTS, nomeando onde já está. Na conferência da nota, cEAN e cEANTrib normalizados são procurados nesses três lugares, e a divergência entre o GTIN da nota e o nível de embalagem da linha é alerta de conferência, nunca conversão.
Item aprovado não sai de compra por alteração: a alteração material vira revisão pendente
Num item Approved, o PUT que toca campo material — categoria, tipo, unidade base (enquanto a RN-ITM-10 permitir), atributos de identidade, gtin, manufacturerName, mpn — não altera o item: grava a revisão pendente (pendingRevision, documento com os valores propostos, e revisionNumber). Há no máximo uma. Enquanto ela existe o item segue Approved, a RN-ITM-30 lê só os valores aprovados, e todo documento usa os valores aprovados. POST /catalog/items/{id}/revision/submit abre uma instância nova do processo ITEM sobre a revisão, com as camadas 1 a 4 da antiduplicidade refeitas nos valores revisados; aprovada, o contrato de desfecho aplica a revisão na mesma transação — descrição e assinaturas recalculadas, version incrementado, evento item.revision-applied —; rejeitada, a revisão é descartada com o motivo, e o item fica como estava. Revisão em aprovação não é editada (409 ITEM_REVISION_IN_APPROVAL_CONFLICT); DELETE /catalog/items/{id}/revision descarta a revisão e, se ela estiver em aprovação, cancela a instância. Campo não material grava direto e não cria revisão.
Aprovar na importação só na carga inicial do catálogo, com janela única e dupla custódia
Fora da carga inicial, todo item importado nasce em Draft e só chega a Approved pela instância do processo ITEM no motor, como o item criado na tela; nenhuma permissão e nenhuma opção de importação aprova sozinha. autoApprove existe só na carga inicial de onboarding do catálogo, exceção declarada. Janela: aberta desde a criação do tenant e fechada, sem reabertura, no primeiro destes três: a confirmação de um lote, o fechamento explícito por quem tem approve:catalog-items (POST /catalog/items:close-initial-load), ou 60 dias corridos da criação do tenant; o fechamento grava catalogInitialLoadClosedAt na configuração do tenant. Fora da janela, importação com autoApprove: true responde 409 ITEM_INITIAL_LOAD_WINDOW_CLOSED_CONFLICT e não começa. Dupla custódia: quem tem create:catalog envia o arquivo por POST /catalog/items:import com autoApprove: true, com dryRun=true antes; sem dryRun, as linhas válidas viram itens em Draft com origin = InitialLoad, o mesmo initialLoadBatchId e o hash do arquivo — nenhum comprável ainda, e linha que casa em camada bloqueante da antiduplicidade fica no relatório, sem o forçar. Até a confirmação, item do lote não é editado nem submetido ao motor (409 ITEM_INITIAL_LOAD_PENDING_CONFLICT). Outra pessoa, com approve:catalog-items, confirma por POST /catalog/items:confirm-initial-load informando lote e hash; a mesma pessoa que importou responde 409 ITEM_INITIAL_LOAD_APPROVER_IS_IMPORTER_CONFLICT, hash diferente 409 ITEM_INITIAL_LOAD_HASH_MISMATCH_CONFLICT. A confirmação passa o lote inteiro a Approved numa transação, sem instância do motor, fecha a janela, grava o registro do ato de controle — quem importou, quem confirmou, lote, hash e contagem —, isolado por tenant e na mesma transação, e publica item.initial-load-confirmed depois do commit. Lote não confirmado quando a janela fecha não se perde: os itens ficam em Draft, liberados para edição, e seguem pela aprovação normal. Por quê: o processo ITEM não admite aprovação por política; sem janela e sem segunda pessoa, quem tem approve:catalog-items escreveria o catálogo aprovado sozinho, a qualquer tempo, por planilha.
O item expõe três flags ao motor, e só o dado pessoal é marcado à mão
As flags que o motor de aprovação lê do item são calculadas pelo dono na hora do ato, nunca gravadas como coluna própria: regulated quando a categoria do item declara ou herda requiredComplianceKinds, ou quando controlKind é diferente de None; critical quando criticality é Strategic ou Bottleneck, os dois quadrantes de alto risco de fornecimento; personalData quando handlesPersonalData é verdadeiro. No processo ITEM elas são flags do assunto; nos documentos com linhas, cada linha as leva em itemFlags (Fluxo de Aprovação, §Contrato de submissão). Linha não catalogada não tem nenhuma (RN-ITM-36).
Por quê. Com o nome das flags só em prosa e sem origem no item, a política "software com dado pessoal exige o DPO" não podia ser montada: ninguém sabia de onde o dono tiraria o valor. Derivar regulated e critical do que o item já tem evita um segundo campo que diverge do primeiro; o dado pessoal não se deriva de nada, e por isso é o único campo novo.
Limite declarado. handlesPersonalData responde pelo que é intrínseco ao produto — o CRM, o relógio de ponto. O serviço genérico que às vezes trata dado pessoal, como uma consultoria, não é coberto aqui: a marca declarada na linha é da spec da Requisition.
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:import | create:catalog | Assíncrono, com dryRun=true antes. Itens nascem em Draft; autoApprove: true só dentro da janela de carga inicial, e ainda assim cria o lote a confirmar (RN-ITM-42). | 400 · 409 |
| POST /catalog/items:confirm-initial-load | approve:catalog-items | Segunda custódia: outra pessoa confirma lote e hash; aprova o lote e fecha a janela (RN-ITM-42). | 409 |
| POST /catalog/items:close-initial-load | approve:catalog-items | Fecha a janela de carga inicial sem carga. Não reabre. | 409 |
| POST /catalog/items/{id}/submit | create:catalog | ItemDto em PendingApproval, com approvalRequestId. Abre a instância ITEM no motor | 409 · 422 |
| POST /catalog/items/{id}/revision/submit | update:catalog | ItemDto ainda Approved, com a revisão em aprovação. Abre instância nova do processo ITEM sobre a revisão (RN-ITM-41) | 404 · 409 · 422 |
| DELETE /catalog/items/{id}/revision | update:catalog | 204. Descarta a revisão pendente; em aprovação, cancela a instância (RN-ITM-41) | 404 · 409 |
| sem rota própria | — | Aprovar e rejeitar é decisão do motor, na tarefa da instância; o item aplica Approved ou Rejected ao terminar. | — |
| 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. |
| 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. |
| 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 | create:catalog-suppliers | Cria a relação com o conjunto de unidades de compra (ao menos uma, RN-ITM-33). Preço, quando enviado junto, exige create:catalog-prices adicionalmente. |
| PUT /catalog/items/{id}/suppliers/{supplierItemId} | update:catalog-suppliers | Concorrência por expectedVersion. Não carrega unidades de compra, apelidos nem preço — cada um tem rota própria. Não aceita status: ver block e unblock. |
| POST /catalog/items/{id}/suppliers/{supplierItemId}/block · /unblock | update:catalog-suppliers | Muda o status da relação (RN-ITM-15). Motivo codificado obrigatório nos dois sentidos — SupplierBlock e SupplierUnblock — e expectedVersion. Devolve 200 com a relação. |
| DELETE /catalog/items/{id}/suppliers/{supplierItemId} | delete: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. |
| GET /catalog/items/{id}/suppliers/{supplierItemId}/prices | read:catalog | 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}/prices | create:catalog-prices | 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 e modalidade de frete incoerente com o tipo do item (RN-ITM-27). |
| PUT /catalog/items/{id}/suppliers/{supplierItemId}/prices/{priceId} | update:catalog-prices | 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. |
| PUT /catalog/items/{id}/suppliers/{supplierItemId}/uoms | update:catalog-suppliers | Reescreve o conjunto de unidades de compra da relação, com o expectedVersion dela (RN-ITM-33). Fator diferente para unidade já usada não edita: desativa a linha usada e cria outra na mesma transação (RN-UOM-09). Unidade retirada do conjunto é desativada se já usada e apagada se nunca usada. Devolve o conjunto com os ids novos. |
| PUT /catalog/items/{id}/suppliers/{supplierItemId}/uom-aliases | update:catalog-suppliers | Reescreve o conjunto de apelidos de unidade da nota, com o expectedVersion da relação (RN-ITM-35). Não refaz conferência concluída. |
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 | create: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} | update: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 | update:catalog-categories | 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 | update:catalog-categories | Reativa o nó. Exige a cadeia de ancestrais ativa e vigente; não desce à subárvore. expectedVersion obrigatório. |
| PATCH /catalog/categories/{id}/deactivate | delete: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 | update: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 | create: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 | create: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 |
| 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_GTIN_ALREADY_EXISTS | 409 | GTIN normalizado já identifica outro item, ou o mesmo item com outro fator — a mensagem nomeia onde (RN-ITM-40) |
| ITEM_BASE_UOM_PACKAGING_NOT_ALLOWED | 400 | Unidade base com isPackaging (RN-ITM-10) |
| ITEM_REVISION_IN_APPROVAL_CONFLICT | 409 | Edição ou nova submissão de revisão que já está em aprovação (RN-ITM-41) |
| 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. 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_REQUIRES_CONTRACT_RESERVED_UNPROCESSABLE | 422 | requiresContract = true gravado antes de a spec do Contract liberar o flag — na criação, na edição, na revisão ou na importação |
| ITEM_NOT_PURCHASABLE_UNPROCESSABLE | 422 | 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 | Já existe versão de preço para (relação, UF de destino, empresa, modalidade de frete) com vigência que cruza a informada — a mensagem nomeia a versão que colide |
| ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_REQUIRED | 400 | Versão de preço de item cujo tipo tem requiresGoodsTaxCode sem freightTerms (RN-ITM-27) |
| ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_NOT_APPLICABLE | 400 | freightTerms preenchido em versão de preço de item que não é mercadoria (RN-ITM-27) |
| ITEM_SUPPLIER_PRICE_INCOTERM_NOT_APPLICABLE | 400 | incoterm com fornecedor que não é Foreign, incotermNamedPlace sem incoterm, ou incoterm sem incotermNamedPlace (RN-ITM-27) |
| ITEM_SUPPLIER_PRICE_INCOTERM_FREIGHT_MISMATCH | 400 | freightTerms do lado errado do transporte principal para o incoterm informado — a mensagem nomeia os dois (RN-ITM-27) |
| ITEM_SUPPLIER_PRICE_NOT_EFFECTIVE_UNPROCESSABLE | 422 | Nenhuma versão de preço vigente na data de submissão para a relação escolhida, a UF de destino da linha, a empresa do documento e a modalidade de frete pedida — o preço não é sugerido nem aproximado |
| ITEM_SUPPLIER_PRICE_LOCKED_CONFLICT | 409 | Edição de versão de preço já congelada em linha de documento — reajuste é versão nova |
| ITEM_SUPPLIER_BLOCKED_CONFLICT | 409 | Relação em Blocked escolhida para linha de cotação ou de pedido (RN-ITM-15). Fornecedor bloqueado responde pela tabela e pelos códigos do Fornecedor, não por este |
| ITEM_SUPPLIER_STATUS_UNCHANGED_CONFLICT | 409 | block em relação já Blocked, ou unblock em relação Active (RN-ITM-15) |
| ITEM_SUPPLIER_SHIP_TO_STATE_INVALID | 400 | UF fora das 27 unidades federativas |
| ITEM_SUPPLIER_UOM_SET_INVALID | 400 | Conjunto de unidades de compra sem linha, com unidade repetida ou sem exatamente uma padrão (RN-ITM-33) |
| ITEM_SUPPLIER_UOM_SERVICE_BASE_ONLY | 400 | Item de base Service com unidade de compra diferente da base ou fator diferente de 1/1 (RN-ITM-33) |
| ITEM_SUPPLIER_UOM_ALIAS_IS_UOM_CODE_UNPROCESSABLE | 422 | Apelido cujo texto normalizado já é código de unidade do Nexio (RN-ITM-35) |
| ITEM_SUPPLIER_UOM_ALIAS_TARGET_UNRESOLVABLE_UNPROCESSABLE | 422 | Apelido apontando para unidade sem fator para a base do item (RN-ITM-35) |
| ITEM_SUPPLIER_UOM_ALIAS_LIMIT_EXCEEDED | 400 | Mais de 20 apelidos na relação (RN-ITM-35) |
| ITEM_COMPLIANCE_EXPIRED_CONFLICT | 409 | Compra de item com compliance vencido e bloqueante |
| ITEM_UNCATALOGUED_LINE_INCOMPLETE | 400 | Linha não catalogada sem descrição, categoria, unidade ou destinação — a mensagem nomeia o campo (RN-ITM-36) |
| ITEM_UNCATALOGUED_LINE_CATEGORY_UNPROCESSABLE | 422 | Linha não catalogada em nó inativo, fora da vigência, sem allowsPosting ou com exigência regulatória declarada ou herdada (RN-ITM-36) |
| ITEM_INITIAL_LOAD_WINDOW_CLOSED_CONFLICT | 409 | Importação com autoApprove, confirmação ou fechamento com a janela de carga inicial do catálogo já fechada (RN-ITM-42) |
| ITEM_INITIAL_LOAD_APPROVER_IS_IMPORTER_CONFLICT | 409 | Confirmação de lote de carga inicial pela mesma pessoa que o importou (RN-ITM-42) |
| ITEM_INITIAL_LOAD_HASH_MISMATCH_CONFLICT | 409 | Confirmação de lote com hash diferente do enviado (RN-ITM-42) |
| ITEM_INITIAL_LOAD_PENDING_CONFLICT | 409 | Edição ou submissão de item de lote de carga inicial ainda não confirmado (RN-ITM-42) |
Autorização
Permissões
Padrão do produto, conferido contra o código da plataforma: <verbo>:<recurso>[-<sub-recurso>] — verbo em kebab-case singular, recurso em kebab-case plural. O plural alinha ao catálogo semeado do GrydAuth (read:users, update:tenants, create:permissions). 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, 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 |
| export:catalog · clone:catalog | existe | Exportação e clonagem. A importação (POST /catalog/items:import) é criação de item em lote e pede create:catalog |
| create:catalog-categories | existe | Criar nó da árvore de categorias |
| update:catalog-categories | existe | Renomear, reordenar, mover, reativar nó e vincular atributos; também a exceção de allowsPosting |
| delete:catalog-categories | existe | Desativar a subárvore (PATCH …/deactivate, retirada lógica) |
| create:catalog-suppliers | existe | Criar a relação ItemSupplier |
| update:catalog-suppliers | existe | Editar a relação, as unidades de compra e os apelidos; bloquear e desbloquear |
| delete:catalog-suppliers | existe | Apagar a relação (DELETE) |
| create:catalog-prices | existe | Gravar versão de preço nova do fornecedor (ItemSupplierPrice) e informar o preço de referência na criação do item. Criar a relação é create:catalog-suppliers; gravar preço nela exige esta, adicionalmente |
| update:catalog-prices | existe | Corrigir versão de preço ainda não usada e alterar o preço de referência do item |
| 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 |
| create:catalog-attributes | novo | Criar definição do dicionário de atributos |
| update:catalog-attributes | novo | Editar definição do dicionário de atributos |
| delete:catalog-attributes | novo | Retirar definição do dicionário de atributos |
| create:catalog-item-types | novo | Criar tipo de item |
| update:catalog-item-types | novo | Editar tipo de item e fases do ciclo de vida |
| delete:catalog-item-types | novo | Retirar tipo de item |
Grupos sugeridos: Nexio - Catálogo (read/create/update/clone/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, freight_terms, lead_time, moq | opc | Cria o ItemSupplier na mesma linha, com a unidade de compra como ItemSupplierUom padrão e o preço como versão de ItemSupplierPrice. Repetir a linha com fornecedor diferente adiciona vínculo. |
| freight_terms | cond | Modalidade de frete da versão de preço, pelo código do modFrete (0, 1, 2, 3, 4, 9) ou pelo nome do valor. Obrigatória quando a linha traz price e o tipo do item tem requiresGoodsTaxCode; vazia nos demais, inclusive em serviço (RN-ITM-27). Faltando, a linha vai ao relatório com ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_REQUIRED; preenchida onde não se aplica, com ITEM_SUPPLIER_PRICE_FREIGHT_TERMS_NOT_APPLICABLE — em nenhum dos dois casos o preço é criado. |
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
Sempre, e sem permissão que dispense: importação que aprova transforma a planilha do cliente em verdade sem revisão, e o processo ITEM não admite aprovação por política. Cada item segue para a curadoria no motor como o criado na tela. A única exceção é a carga inicial de onboarding do catálogo — janela única por tenant, quem importa ≠ quem confirma, evento próprio e ato de controle —, e mesmo nela os itens ficam em Draft até a confirmação do lote (RN-ITM-42).
Encadeamento
O que o item entrega ao fluxo de aprovação
O fluxo de aprovação é impactado por este cadastro em seis 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. O requiresContract só entra quando a spec do Contract liberar o flag; até lá ele é sempre false. As três flags que o motor lê hoje — regulated, personalData e critical — são derivadas pela RN-ITM-43. |
| 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. |
| isUncatalogued | Marca a linha com itemId nulo (RN-ITM-36). Categoria e destinação continuam valendo como em qualquer linha; a marca é o critério com que o tenant exige aprovação para compra fora do catálogo, sem código. |
A curadoria também é uma instância do motor. 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ó. 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 — decompor a versão de preço em mercadoria, frete, seguro, embalagem especial, ferramental e tributo não recuperável. Gancho nomeado:
ItemSupplierPriceComponent, filha deItemSupplierPrice. Enquanto a modalidade de frete responder "o que este preço inclui", a entidade não nasce — o gatilho é o comparativo precisar somar componente que ela não cobre. - Contrato — o
ContractItemda especificação do Contrato referencia item ou relação, com empresa e vigência (RN-ITM-34). OItemSuppliernão guarda contrato. - 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. - Multi-empresa. resolvido em 01/09/2026:
CompanyeEstablishmentvivem dentro do tenant, e o vocabulário do catálogo não ganha empresa — item, tipo, categoria e unidade continuam sendo do tenant inteiro. Era o desfecho que mais interessava ao catálogo, e é o que o modo consolidado entrega. A condição comercial não é vocabulário, e por isso a versão de preço ganhoucompanyIdanulável na chave (RN-ITM-28) — a v1 pode gravar sempre nulo. Ver Empresa e Estabelecimento.
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v3.20 | 30/10/2026 | Permissões padronizadas em read/lookup/create/update/delete (decisão de 30/10/2026). Mapeamento aplicado, por ação: manage:catalog-categories → create (POST /catalog/categories), update (PUT, PATCH …/parent, PATCH …/activate, PUT …/attributes, exceção de allowsPosting) e delete (PATCH …/deactivate); manage:catalog-suppliers → create (POST …/suppliers), update (PUT da relação, …/uoms, …/uom-aliases, block e unblock) e delete (DELETE da relação); manage:catalog-prices → create (versão de preço nova; preço de referência na criação do item) e update (correção de versão não usada; preço de referência alterado); manage:catalog-attributes e manage:catalog-item-types → create, update e delete do recurso; import:catalog → create:catalog (importação é criação em lote). Nenhuma regra de negócio mudou. |
| v3.19 | 21/09/2026 | Validação de 18/09, achado 20 (decisão do Roger). requiresContract fica reservado até a spec do Contract: gravar true responde ITEM_REQUIRES_CONTRACT_RESERVED_UNPROCESSABLE, código novo. A regra “item só vira pedido sob contrato vigente” sai daqui e passa a ser da spec do Contract, que libera o flag. A spec do Contract vem antes do Pedido ou junto com ele, e por isso a nota da recorrência continua dizendo que vigência, renovação, aviso prévio e reajuste vêm do contrato. Achado 23 (decisão do Roger): campo novo handlesPersonalData e RN-ITM-43 — as três flags que o motor lê do item ganham nome e origem: regulated (requiredComplianceKinds da categoria ou controlKind), critical (criticality Strategic ou Bottleneck) e personalData (o campo novo). A marca declarada na linha, para serviço genérico, é da spec da Requisition. |
| v3.18 | 20/09/2026 | Correção da validação de 18/09 (achado 18a). A tolerância de recebimento da relação deixa de ser dois percentuais repetidos aqui e passa a ser o objeto receiptTolerance, citado da Unidade de Medida — que é a dona do objeto, da precedência resolvida campo a campo e da avaliação em unidade base. |
| v3.17 | 20/09/2026 | Validação de 18/09, achados 13 e 14 (decisões do Roger). ItemSupplierStatus: New | UnderQualification | Approved | Blocked → Active | Blocked — New e UnderQualification não tinham ato que os movesse nem consumidor, porque a autorização de gasto é a qualificação por categoria do Fornecedor; homologação por item volta como valor novo quando tiver consumidor. Entra a tabela de efeito (allowsQuotation e allowsOrder), as rotas block e unblock com motivo e trilha, os campos blockedAt e blockedBy e o código ITEM_SUPPLIER_STATUS_UNCHANGED_CONFLICT; ITEM_SUPPLIER_BLOCKED_CONFLICT passa a ser só da relação. RN-ITM-15: o bloqueio do fornecedor inteiro deixa de reescrever as relações por evento (SupplierStatusChangedEvent, que o Fornecedor não publica) e passa a valer pela leitura do eixo Bloqueio de lá — sem evento e sem “status anterior”. |
| v3.16 | 11/09/2026 | Correção da validação R2. RN-ITM-42: autoApprove só na carga inicial do catálogo — janela única por tenant (60 dias, confirmação ou fechamento), quem importa ≠ quem confirma, origin/initialLoadBatchId, evento item.initial-load-confirmed, ato de controle; rotas :confirm-initial-load e :close-initial-load; quatro erros ITEM_INITIAL_LOAD_*. Importação ganha a coluna freight_terms, obrigatória com preço de mercadoria. Forçar duplicata registrado como ato de controle; eixos do preço com UF e ordem dos degraus; marcas "Novo." e datas de revisão fora do corpo. |
| v3.15 | 11/09/2026 | Validação de 10/09 e Ata B15. Relação ganha três filhas: ItemSupplierUom (unidades de compra, sem vigência, linha usada imutável), ItemSupplierUomAlias (unidade escrita na nota) e a versão de preço; contractId sai da relação (futuro ContractItem); purchaseUomId, fator, supplierUomLabel e múltiplos vão para a linha de unidade. Chave do preço troca incoterm por freightTerms? (vocabulário do modFrete); incoterm opcional, fora da chave, só Foreign. Uma regra para fator usado (sem CX10 obrigatório). Linha não catalogada, ordem total dos degraus de preço (empresa antes da UF), faixa pela unidade de preço, versão de preço com fator congelado e ligada à linha de embalagem, linha sem relação com fator declarado aceito, GTIN por nível de embalagem, revisão pendente de item aprovado (substitui a reabertura por revaluate), atributo de identidade e forma canônica da assinatura, código no fornecedor opcional com camada 3 só contra outro item, base nunca embalagem e um gatilho só para a imutabilidade, preço decimal(21,10). RN-ITM-10, 24, 25, 26, 27, 29 e 31 reescritas; RN-ITM-33 a 41 novas. Marcas de histórico retiradas do corpo. |
| v3.12 – v3.14 | 09/09/2026 | Relação e versão de preço partidas em duas entidades, UF e empresa compradora na chave do preço, incoterm obrigatório (revogado na v3.15), fator da unidade de preço congelado na linha, três eixos conjuntivos na compra, assinatura com nome e categoria, conversão interclasse declarada pelo item, reabertura pelo motor, permissões no plural. |
| v3.9 – v3.11 | 08/09/2026 | Árvore de categorias com a mecânica do Centro de Custo (allowsPosting, vigência, level = depth − 1, PATCH /parent); vigência do preço na chave e fator em uso imutável; teto de profundidade vira gancho declarado; motivo codificado substitui rejectionReason; DELETE de foto libera a FileReference. |
| v3.6 – v3.8 | 04 – 06/09/2026 | Curadoria como instância do motor (saem approve/reject próprios); token de concorrência na categoria; linha de documento passa a <Documento>Item; item não é composto de item. |
| até v3.5 | 29/08 – 03/09/2026 | Primeira especificação (29/08), supplierEstablishmentId nomeado (01/09), UF de destino no lugar do shipToLocationId (02/09), foto por upload direto ao GrydFiles (03/09). |