Skip to content

Módulo nfe-import

Responsabilidade

Processa NF-es fiscais (de fontes manuais ou automáticas) e converte seus itens em movimentações de estoque precisas, com mapeamento inteligente de produtos, conversão de unidade de medida, rateio de frete e resolução de conflitos.

Complementa o módulo inbound-nfe (que gerencia a manifestação do destinatário junto à SEFAZ).

Localização

backend/src/nfe-import/

Serviços

NfeImportPipelineService

Orquestra os 11 passos do pipeline. Ponto de entrada único para ambas as origens.

typescript
run(opts: PipelineRunOptions): Promise<PipelineResult>
// opts.mode = 'manual' | 'auto'
// manual: fornece xmlContent (string)
// auto: fornece nfeId (ObjectId de InboundNfe)

applySession(session, parsedNfe, supplier, branchId, nfeDoc, tenantId, userId?): Promise<void>
// Aplica uma ImportSession com status='ready' ao estoque
// Idempotente via session.lastAppliedItemIndex

NfeImportFuzzyService

Estratégias de matching em ordem de prioridade:

typescript
matchItem(tenantId, supplierId, nfeItem): Promise<MatchResult>
// 1. EXACT: SupplierProductMap { supplierCode: cProd } → score=100
// 2. FUZZY: InventoryItem por NCM + Jaro-Winkler + histórico fornecedor
//    score = ncmBonus(40) + jaroWinkler(name, description)×50 + supplierHistory(10)

Implementa Jaro-Winkler internamente (~40 linhas puro TS, sem dependência externa). Normaliza strings: minúsculas, remove acentos, substitui non-alphanum por espaço.

NfeImportValidatorService

typescript
parseXml(xmlContent: string): ParsedNfe
// Usa fast-xml-parser (já instalado), suporta nfeProc v4.0
// Extrai: nfeKey, emitterCnpj, cnpjDestinatario, vTotal, vFrete, vDesc, vOutro, items[]

validateFiscal(nfe: ParsedNfe): FiscalValidation
// Verifica integridade do total (tolerância R$0,01)
// Flags CFOP de saída (5.xxx/6.xxx) e devolução (1.201/2.201)
// Valida formato NCM (8 dígitos)

isReturnNote(cfop: string): boolean
// true para CFOP 1.201 / 2.201

NfeImportRouterService

typescript
resolveBranch(tenantId, cnpjDestinatario): Promise<{ branchId, warning }>
// Branch.cnpj match → encontrado: usa essa filial
//                   → não encontrado: HQ + warning

resolveLocation(tenantId, branchId, category): Promise<string | null>
// RoutingRule { tenant, branch, category } → targetLocation
// Ordenado por priority ASC

// CRUD de RoutingRule:
listRules / createRule / updateRule / deleteRule

NfeImportSessionService

typescript
findById(tenantId, sessionId): Promise<ImportSessionDocument>
applyResolutions(tenantId, sessionId, dto: ResolveConflictsDto): Promise<ImportSessionDocument>
// Para cada resolução: accept | remap (muda inventoryItem) | skip
// Recalcula qtyToStock e costPerStockUnit se conversionFactor foi sobrescrito

getLedger(tenantId, sessionId): Promise<LedgerEntry[]>
// Só disponível quando status='applied'

NfeImportWorkerProcessor

BullMQ @Processor('nfe-import-auto', { concurrency: 2 }) — processa jobs do modo automático.

typescript
process(job: Job<NfeImportJobData>): Promise<void>
// job.data = { nfeId, tenantId }
// Chama NfeImportPipelineService.run({ mode:'auto', nfeId, tenantId })
// Na exaustão (3 tentativas): InboundNfe.status = 'desconhecida'

Schemas MongoDB

SupplierProductMap

Tabela de junção aprendida N:N fornecedor↔produto.

typescript
{
  tenant: ObjectId
  supplier: ObjectId      // ref Supplier
  inventoryItem: ObjectId // ref InventoryItem
  supplierCode: string    // cProd da NF-e
  supplierUnit: string    // unidade de compra (ex: 'CX', 'DZ')
  conversionFactor: number // min:1 — 1 supplierUnit = N unidades de estoque
  lastSeenPrice: number   // custo/supplierUnit na última NF-e
  lastSeenPriceAt: Date
  ncm: string
  supplierDescription: string
  confirmedAt: Date | null // null=auto-aprendido; Date=confirmado pelo usuário
  confirmedBy: ObjectId | null
  active: boolean
}
// Índices:
// { tenant, supplier, supplierCode } UNIQUE
// { tenant, inventoryItem }

ImportSession

Sessão de importação com TTL de 24h (MongoDB auto-deleta sessões abandonadas).

typescript
{
  tenant, inboundNfe, branch?,
  mode: 'manual' | 'auto',
  status: 'pending' | 'conflict' | 'ready' | 'applied' | 'failed',
  fiscalValidation: FiscalValidation,
  routingWarnings: string[],
  items: ImportSessionItem[],   // embedded
  ledger: LedgerEntry[],        // populated após apply
  lastAppliedItemIndex: number?, // resume idempotente
  expiresAt: Date               // TTL index expireAfterSeconds: 0
}

RoutingRule

typescript
{
  tenant: ObjectId
  branch: ObjectId
  category: string        // valor de InventoryItem.category
  targetLocation: string  // ex: 'Câmara Fria', 'Adega'
  priority: number        // menor = maior prioridade
}
// Índice: { tenant, branch, category } UNIQUE

Adições ao schema InboundNfe

typescript
cnpjDestinatario: string  // extraído do XML no primeiro import
routedToBranch: ObjectId? // ref Branch — filial roteada
importSessionId: ObjectId? // ref ImportSession — sessão vinculada

Integração com Outros Módulos

DependênciaComo usada
InventoryModuleInventoryService.createMovement() para registrar entrada/saída
BomModuleBomService.recalculateAllForIngredients() fire-and-forget após apply
InboundNfe modelLeitura para modo auto; atualiza inventoryProcessed, routedToBranch
AccountsPayable modelCria AP diretamente via model (sem service intermediário)
Supplier modelfindOrCreate por CNPJ emitente
PurchaseOrder modelTenta vincular e chamar receiveItems() para POs abertos
Branch modelResolve CNPJ Destinatário → filial

Modo Manual vs Automático

AspectoManual (import-xml)Automático (nfe-import-auto)
EntradaXML upload via multipartnfeId do BullMQ job
RetornoImportSessionResult síncronoAssíncrono, atualiza InboundNfe
ConflitosExibe Conflict Resolution UIMarca desconhecida, badge na lista
RetryN/A (usuário controla)3× exponencial (30s/5min/30min)

Idempotência e Resiliência

  • Duplicate check: { nfeKey, tenant } unique index em InboundNfe bloqueia segundo import
  • Partial apply recovery: session.lastAppliedItemIndex persisted a cada item → resume do ponto de falha
  • Abandoned sessions: TTL 24h via índice MongoDB — InboundNfe.inventoryProcessed permanece false, nota disponível para retry
  • Cross-tenant isolation: todas queries de Branch/InventoryItem sempre com { tenant } — impossível vazar entre tenants

Controller — Endpoints

Base import: @Controller('t/:slug/inbound-nfe')Routing rules: @Controller('t/:slug/inventory/routing-rules')Supplier maps: @Controller('t/:slug/inventory/supplier-maps')

Guards: JwtAuthGuard → TenantGuard → PermissionsGuard(inventory.*)

Ver tabela completa em docs/admin/inbound-nfe.md.

Configuração do Módulo

typescript
// nfe-import.module.ts registra:
BullModule.registerQueue({
  name: 'nfe-import-auto',
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: 'exponential', delay: 30000 },
  },
})
// forwardRef(InventoryModule), forwardRef(BomModule)
// para evitar dependência circular

Relacionados

Lançado sob a licença MIT.