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.
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.lastAppliedItemIndexNfeImportFuzzyService
Estratégias de matching em ordem de prioridade:
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
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.201NfeImportRouterService
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 / deleteRuleNfeImportSessionService
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.
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.
{
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).
{
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
{
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 } UNIQUEAdições ao schema InboundNfe
cnpjDestinatario: string // extraído do XML no primeiro import
routedToBranch: ObjectId? // ref Branch — filial roteada
importSessionId: ObjectId? // ref ImportSession — sessão vinculadaIntegração com Outros Módulos
| Dependência | Como usada |
|---|---|
InventoryModule | InventoryService.createMovement() para registrar entrada/saída |
BomModule | BomService.recalculateAllForIngredients() fire-and-forget após apply |
InboundNfe model | Leitura para modo auto; atualiza inventoryProcessed, routedToBranch |
AccountsPayable model | Cria AP diretamente via model (sem service intermediário) |
Supplier model | findOrCreate por CNPJ emitente |
PurchaseOrder model | Tenta vincular e chamar receiveItems() para POs abertos |
Branch model | Resolve CNPJ Destinatário → filial |
Modo Manual vs Automático
| Aspecto | Manual (import-xml) | Automático (nfe-import-auto) |
|---|---|---|
| Entrada | XML upload via multipart | nfeId do BullMQ job |
| Retorno | ImportSessionResult síncrono | Assíncrono, atualiza InboundNfe |
| Conflitos | Exibe Conflict Resolution UI | Marca desconhecida, badge na lista |
| Retry | N/A (usuário controla) | 3× exponencial (30s/5min/30min) |
Idempotência e Resiliência
- Duplicate check:
{ nfeKey, tenant }unique index emInboundNfebloqueia segundo import - Partial apply recovery:
session.lastAppliedItemIndexpersisted a cada item → resume do ponto de falha - Abandoned sessions: TTL 24h via índice MongoDB —
InboundNfe.inventoryProcessedpermanecefalse, 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
// 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