{
  "name": "Atlas Gestão: Conferência de NF-e v8 (MVP local)",
  "nodes": [
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Atlas Gestão: monta a linha da planilha (nomes de campo = cabeçalhos das abas)\nconst d = $input.item.json;\nconst itens = JSON.parse(d.itens || '[]');\nlet parcelas = [];\ntry { parcelas = JSON.parse(d.parcelas || '[]'); } catch (e) { parcelas = []; }\nlet arquivo = 'arquivo_desconhecido';\ntry { arquivo = $('Detectar Formato').item.json.nome_arquivo; } catch (e) { arquivo = $('Detectar Formato').first().json.nome_arquivo; }\n\nconst digits = s => String(s || '').replace(/\\D/g, '');\nconst fmtDoc = s => {\n  const x = digits(s);\n  if (x.length === 14) return x.replace(/^(\\d{2})(\\d{3})(\\d{3})(\\d{4})(\\d{2})$/, '$1.$2.$3/$4-$5');\n  if (x.length === 11) return x.replace(/^(\\d{3})(\\d{3})(\\d{3})(\\d{2})$/, '$1.$2.$3-$4');\n  return String(s || '');\n};\n// raiz do CNPJ (8 primeiros dígitos) com pontos: continua texto na planilha e preserva o zero à esquerda\nconst fmtRaiz = s => { const x = digits(s); return x.length === 14 ? `${x.slice(0, 2)}.${x.slice(2, 5)}.${x.slice(5, 8)}` : ''; };\n// data ISO -> número serial do Google Sheets (a coluna da planilha já vem formatada como data)\nconst serial = iso => {\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(iso || '');\n  return m ? Math.round(Date.UTC(+m[1], +m[2] - 1, +m[3]) / 86400000) + 25569 : '';\n};\nconst brl = v => Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });\nconst agora = new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' });\nconst ok = d.status === 'ok';\nconst ehServico = ['NFS-e', 'NFPS-e'].includes(d.tipo_documento);\n\n// ---- classificação gerencial (plano de contas simplificado da Atlas para PMEs) ----\n// 1) regra por fornecedor vence tudo. Preencha por cliente: CNPJ raiz (8 dígitos) -> categoria\nconst REGRAS_FORNECEDOR = {\n  // '13986197': 'Material de escritório e livros',\n};\n// 2) produto: NCM (capítulo/posição do Sistema Harmonizado). A ordem importa: o primeiro que casar vence\nconst CAT_NCM = [\n  [/^(33|3401|3402|3808|4818|9603)/, 'Limpeza e higiene'],\n  [/^(61|62|63|64|65|4015|9020)/, 'Uniformes e EPI'],\n  [/^(30|9018|9019|9021|9022)/, 'Saúde e farmácia'],\n  [/^(0[1-9]|1\\d|2[0-4])/, 'Alimentação e copa'],\n  [/^27/, 'Combustíveis e lubrificantes'],\n  [/^(8471|8517|8518|8519|8521|8523|8525|8528|8443|8504|8507|8509|8516|8536|8544)/, 'Informática e eletrônicos'],\n  [/^(84|85|90)/, 'Equipamentos e máquinas'],\n  [/^(48|49)/, 'Material de escritório e livros'],\n  [/^(94|69|70|73|39|76|44|82)/, 'Móveis e utilidades'],\n];\n// 3) serviço (NFS-e não tem NCM): palavras da descrição, sem acento. O primeiro que casar vence\nconst CAT_SERVICO = [\n  [/software|licenc|assinatura|saas|aplicativo|desenvolvimento|programac|hospedagem de (site|servidor|dados)|nuvem|cloud|dominio|suporte tecnico|\\bti\\b/, 'Software e tecnologia'],\n  [/marketing|publicidade|propaganda|anuncio|midia|trafego|social media|design|criacao|fotograf|video/, 'Marketing e publicidade'],\n  [/contab|juridic|advoc|consultoria|assessoria|auditoria|honorario|bpo|pericia/, 'Serviços profissionais'],\n  [/telefon|internet|banda larga|telecom|celular|link dedicado/, 'Telecom e internet'],\n  [/energia|agua|esgoto|saneamento|\\bgas\\b/, 'Energia, água e gás'],\n  [/frete|transporte|logistica|entrega|motoboy|carreto|courier/, 'Frete e logística'],\n  [/aluguel|locacao|condominio|iptu/, 'Aluguel e condomínio'],\n  [/manutencao|reparo|conserto|instalacao|limpeza|dedetiz|jardinagem|vigilancia/, 'Manutenção e limpeza'],\n  [/treinamento|curso|capacitacao|palestra|workshop|evento|inscricao/, 'Treinamento e eventos'],\n  [/hotel|passagem|aereo|diaria|hospedagem|viagem|translado/, 'Viagens e deslocamento'],\n  [/seguro|corretagem/, 'Seguros'],\n];\nconst norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();\nconst raizFornecedor = digits(d.emitente_cnpj).slice(0, 8);\nconst categoriaDe = (item) => {\n  if (REGRAS_FORNECEDOR[raizFornecedor]) return REGRAS_FORNECEDOR[raizFornecedor];\n  const x = digits(item.ncm);\n  if (x) {\n    for (const [re, nome] of CAT_NCM) if (re.test(x)) return nome;\n    return 'Outros';\n  }\n  const texto = norm(`${item.descricao} ${d.natureza_operacao}`);\n  for (const [re, nome] of CAT_SERVICO) if (re.test(texto)) return nome;\n  return ehServico ? 'Serviços (a classificar)' : 'Outros';\n};\n\n// ---- a nota gera pagamento ou recebimento? (alimenta o fluxo de caixa) ----\nconst natureza = norm(d.natureza_operacao);\nconst movimentaCaixa = /bonificacao|brinde|doacao|amostra|demonstracao|comodato|conserto|transferencia/.test(natureza) ? 'Não'\n  : /remessa|devolucao|consignacao|retorno/.test(natureza) ? 'Verificar'\n  : 'Sim';\nconst vencimentos = parcelas.map(p => p.vencimento).filter(Boolean).sort();\nconst itensCat = itens.map(i => ({ ...i, categoria: categoriaDe(i) }));\nconst porCat = {};\nfor (const i of itensCat) porCat[i.categoria] = (porCat[i.categoria] || 0) + (Number(i.valor_total) || 0);\nconst catNota = (Object.entries(porCat).sort((a, b) => b[1] - a[1])[0] || [])[0] || (ehServico ? 'Serviços (a classificar)' : 'Outros');\n\nreturn {\n  json: {\n    'Chave de acesso': d.chave_acesso || `SEM-CHAVE-${arquivo}`,\n    'Data': serial(d.data_emissao),\n    'Mês': (d.data_emissao || '').slice(0, 7),\n    'Número': Number(d.numero) || d.numero,\n    'Série': Number(d.serie) || d.serie,\n    'Emitente': d.emitente_nome,\n    'CNPJ do emitente': fmtDoc(d.emitente_cnpj),\n    'Destinatário': d.destinatario_nome,\n    'Valor total': Number(d.valor_total),\n    'Qtd. itens': Number(d.qtd_itens),\n    'Processado em': agora,\n    // ---- colunas novas (v4) ----\n    'Tipo': d.tipo_documento || '',\n    'Origem': d.origem || '',\n    'Categoria': catNota,\n    'CNPJ raiz': fmtRaiz(d.emitente_cnpj),\n    'Emitente na nota': d.emitente_nome,\n    'Doc. destinatário': fmtDoc(d.destinatario_doc),\n    'Natureza': d.natureza_operacao || '',\n    'ICMS': Number(d.valor_icms) || 0,\n    'Arquivo': arquivo,\n    'Motivo': d.erros,\n    // ---- colunas novas (Atlas) ----\n    'Direção': d.direcao || '',\n    'Movimenta caixa': movimentaCaixa,\n    'Vencimento': vencimentos.length ? serial(vencimentos[0]) : '',\n    'Ação sugerida': d.acao_sugerida || '',\n    status: d.status,\n    mensagem: ok\n      ? `Nota ${d.numero} registrada: ${d.emitente_nome}, ${brl(d.valor_total)}.${d.direcao ? ' ' + d.direcao + '.' : ''}`\n      : `Nota ${d.numero || '(sem número)'} precisa de revisão: ${d.erros}. Ela só entra no fluxo de caixa depois de conferida.`,\n    _itens: itensCat,\n    _parcelas: parcelas,\n  },\n};\n"
      },
      "id": "9449abf1-5de1-406a-9df8-6eb4b635198d",
      "name": "Montar linhas",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        544
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c-e77b2cd7",
              "leftValue": "={{ String($json.reler_com_ia === true) }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5ac3c0e6-812d-49e7-92b4-970c72718a5c",
      "name": "Reler com IA?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1360,
        544
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Validação ÚNICA para os caminhos (XML, PDF por regras e IA). Reprovadas vão para \"revisar\".\nconst CONFERIR_VNF = true;   // confere o total da nota pela fórmula fiscal; ponha false para desligar\nconst d = $input.item.json;\nconst erros = [];\n\nconst digitos = s => String(s || '').replace(/\\D/g, '');\nconst r2 = n => Math.round(n * 100) / 100;\nconst n = v => Number(v) || 0;\n\n// CNPJ: dois dígitos verificadores (módulo 11)\nconst cnpjValido = (c) => {\n  c = digitos(c);\n  if (c.length !== 14 || /^(\\d)\\1+$/.test(c)) return false;\n  const calc = (base) => {\n    let peso = base.length - 7, soma = 0;\n    for (let i = 0; i < base.length; i++) {\n      soma += Number(base[i]) * peso--;\n      if (peso < 2) peso = 9;\n    }\n    const r = soma % 11;\n    return r < 2 ? 0 : 11 - r;\n  };\n  const d1 = calc(c.slice(0, 12));\n  const d2 = calc(c.slice(0, 12) + d1);\n  return c.endsWith(`${d1}${d2}`);\n};\n\n// CPF: dois dígitos verificadores (módulo 11)\nconst cpfValido = (c) => {\n  c = digitos(c);\n  if (c.length !== 11 || /^(\\d)\\1+$/.test(c)) return false;\n  const calc = (base) => {\n    let soma = 0;\n    for (let i = 0; i < base.length; i++) soma += Number(base[i]) * (base.length + 1 - i);\n    const r = (soma * 10) % 11;\n    return r === 10 ? 0 : r;\n  };\n  const d1 = calc(c.slice(0, 9));\n  const d2 = calc(c.slice(0, 9) + d1);\n  return c.endsWith(`${d1}${d2}`);\n};\n\n// Chave de acesso: 44 dígitos, o último é o dígito verificador (módulo 11, pesos 2 a 9 da direita p/ esquerda)\nconst dvChave = (c43) => {\n  let peso = 2, soma = 0;\n  for (let i = c43.length - 1; i >= 0; i--) {\n    soma += Number(c43[i]) * peso;\n    peso = peso === 9 ? 2 : peso + 1;\n  }\n  const r = soma % 11;\n  return r === 0 || r === 1 ? 0 : 11 - r;\n};\n\n// ---- tipo do documento: o informado pela leitura; se não houver, o modelo dentro da chave (posições 21-22) ----\nconst chave = digitos(d.chave_acesso);\nconst modelo = chave.length === 44 ? chave.slice(20, 22) : '';\nconst tipoPelaChave = { '55': 'NF-e', '65': 'NFC-e', '57': 'CT-e', '67': 'CT-e', '58': 'MDF-e' }[modelo] || '';\nconst informado = d.tipo_documento || '';\nconst tipo = informado || tipoPelaChave || 'NF-e';\n\n// ---- conferência com o cliente da Atlas (só roda se o CNPJ do cliente foi informado no envio) ----\nlet cliente = '';\ntry { cliente = digitos($('Detectar Formato').item.json.cliente_cnpj); } catch (e) { cliente = ''; }\nlet direcao = '';\nconst conferirCliente = () => {\n  if (!cliente) return;\n  if (!cnpjValido(cliente)) { erros.push('CNPJ do cliente informado no envio é inválido'); return; }\n  const raiz = x => digitos(x).slice(0, 8);   // a raiz do CNPJ aceita matriz e filiais\n  const ehEmit = digitos(d.emitente_cnpj).length === 14 && raiz(d.emitente_cnpj) === raiz(cliente);\n  const ehDest = digitos(d.destinatario_doc).length === 14 && raiz(d.destinatario_doc) === raiz(cliente);\n  if (ehEmit && ehDest) direcao = 'Transferência entre filiais';\n  else if (ehEmit) direcao = 'Saída (venda)';\n  else if (ehDest) direcao = 'Entrada (compra)';\n  else erros.push('a nota não envolve o CNPJ do cliente (nem como emitente, nem como destinatário)');\n};\n\n// data de emissão no futuro costuma ser erro de leitura\nconst hojeIso = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Sao_Paulo' });\nconst conferirData = () => {\n  if (/^\\d{4}-\\d{2}-\\d{2}$/.test(String(d.data_emissao || '')) && d.data_emissao > hojeIso) erros.push('data de emissão no futuro');\n};\n\n// o que fazer em cada erro, no idioma do caixa (um texto por tipo de erro)\nconst ACOES = [\n  [/destinatário/, 'Comparar o documento do destinatário com a nota original. Se estiver igual, o erro está na própria nota: pedir a correção ao emitente.'],\n  [/chave de acesso|chave lida|da chave/, 'Conferir a chave de 44 dígitos no arquivo original ou pedir o XML ao emitente.'],\n  [/não envolve|do cliente/, 'Confirmar se a nota pertence a esta empresa ou se foi enviada ao cliente errado.'],\n  [/soma dos itens|total da nota|sem itens|valor total/, 'Conferir os valores no arquivo original antes de lançar.'],\n  [/emitente|prestador|número da nota|data de emissão/, 'Conferir os dados do emitente e a data no arquivo original.'],\n  [/validação automática/, 'Documento fora do escopo da conferência automática: lançar manualmente.'],\n];\nconst acaoSugerida = lista => {\n  const textos = [...new Set(ACOES.filter(([re]) => lista.some(e => re.test(e))).map(([, a]) => a))];\n  return textos.length ? textos.join(' ') : (lista.length ? 'Conferir o arquivo original.' : '');\n};\n\nconst saida = (extra = {}) => {\n  conferirCliente();   // erro de cliente não pede nova leitura: quem decide reler com IA (extra.reler_com_ia) já olhou só a leitura\n  return {\n    json: { ...d, tipo_documento: tipo, status: erros.length ? 'revisar' : 'ok', erros: erros.join('; '), acao_sugerida: acaoSugerida(erros), direcao, reler_com_ia: false, ...extra },\n    binary: $input.item.binary,   // mantém o arquivo: o ramo da IA precisa dele\n  };\n};\n\n// destinatário: valida só se o documento foi informado\nconst validaDestinatario = () => {\n  const doc = digitos(d.destinatario_doc);\n  if (doc.length === 14 && !cnpjValido(doc)) erros.push('CNPJ do destinatário inválido');\n  else if (doc.length === 11 && !cpfValido(doc)) erros.push('CPF do destinatário inválido');\n  else if (doc.length > 0 && doc.length !== 11 && doc.length !== 14) erros.push('documento do destinatário com tamanho inválido');\n};\n\n// ---- NFS-e / NFPS-e (serviço): não tem chave de 44 dígitos nem NCM. A identidade é CNPJ + número + data ----\nif (tipo === 'NFS-e' || tipo === 'NFPS-e') {\n  if (!d.numero) erros.push('número da nota ausente');\n  if (!d.data_emissao) erros.push('data de emissão ausente');\n  if (!d.emitente_nome) erros.push('nome do prestador ausente');\n  if (!cnpjValido(d.emitente_cnpj)) erros.push('CNPJ do prestador inválido');\n  validaDestinatario();\n  conferirData();\n  const valor = n(d.valor_total) || n(d.valor_produtos);\n  if (!(valor > 0)) erros.push('valor do serviço zerado ou ausente');\n\n  // chave sintética: o mesmo serviço lido do XML e do PDF cai na mesma linha em vez de duplicar\n  const sintetica = `NFSE-${digitos(d.emitente_cnpj) || 'SEMCNPJ'}-${d.numero || 'SEMNUM'}-${digitos(d.data_emissao) || 'SEMDATA'}`;\n  const chaveFinal = (chave.length === 44 || chave.length === 50) ? chave : sintetica;\n\n  let itens = [];\n  try { itens = JSON.parse(d.itens || '[]'); } catch (e) { itens = []; }\n  if (!itens.length && valor > 0) {\n    itens = [{ codigo: '', descricao: String(d.natureza_operacao || 'Serviço prestado').slice(0, 300), ncm: '', quantidade: 1, valor_unitario: valor, valor_total: valor }];\n  }\n  return saida({\n    chave_acesso: chaveFinal, valor_total: valor, valor_produtos: valor,\n    qtd_itens: itens.length, soma_itens: r2(itens.reduce((s, i) => s + n(i.valor_total), 0)), itens: JSON.stringify(itens),\n    reler_com_ia: d.origem === 'pdf_texto' && erros.length > 0,   // PDF de serviço lido por regras e reprovado: tenta a IA\n  });\n}\n\n// outros tipos (CT-e, MDF-e): ainda não validamos; ficam em revisão com o motivo claro\nif (!['NF-e', 'NFC-e'].includes(tipo)) {\n  erros.push(tipo === 'desconhecido'\n    ? 'estrutura do documento não reconhecida (esperado NF-e, NFS-e ou NFPS-e)'\n    : `documento do tipo ${tipo}: validação automática ainda não disponível, conferir manualmente`);\n  return saida();\n}\nif (informado && tipoPelaChave && informado !== tipoPelaChave) {\n  erros.push(`tipo do documento (${informado}) não confere com o modelo da chave (${tipoPelaChave})`);\n}\n\n// ---- chave de acesso ----\nif (!/^\\d{44}$/.test(chave)) {\n  erros.push('chave de acesso inválida (esperado 44 dígitos)');\n} else {\n  if (dvChave(chave.slice(0, 43)) !== Number(chave[43])) {\n    erros.push('dígito verificador da chave de acesso não confere');\n  }\n  // o CNPJ do emitente faz parte da chave (posições 7 a 20): pega erro de leitura por IA\n  const cnpjEmit = digitos(d.emitente_cnpj);\n  if (cnpjEmit.length === 14 && chave.slice(6, 20) !== cnpjEmit) {\n    erros.push('CNPJ da chave de acesso não confere com o CNPJ do emitente');\n  }\n  // série, número e mês de emissão também estão na chave (posições 23-25, 26-34 e 3-6): pegam leitura errada\n  const serieChave = Number(chave.slice(22, 25)), numChave = Number(chave.slice(25, 34)), aamm = chave.slice(2, 6);\n  if (digitos(d.numero) && Number(digitos(d.numero)) !== numChave) erros.push(`número da nota (${d.numero}) não confere com a chave de acesso (${numChave})`);\n  if (digitos(d.serie) && Number(digitos(d.serie)) !== serieChave) erros.push(`série (${d.serie}) não confere com a chave de acesso (${serieChave})`);\n  const emissao = String(d.data_emissao || '');\n  if (/^\\d{4}-\\d{2}/.test(emissao) && emissao.slice(2, 4) + emissao.slice(5, 7) !== aamm) {\n    erros.push(`mês de emissão (${emissao.slice(0, 7)}) não confere com a chave de acesso (20${aamm.slice(0, 2)}-${aamm.slice(2)})`);\n  }\n  // o nome do arquivo costuma trazer a chave: se trouxer outra, a leitura errou\n  let arquivo = '';\n  try { arquivo = $('Detectar Formato').item.json.nome_arquivo || ''; } catch (e) { arquivo = ''; }\n  const chaveNoNome = (arquivo.match(/\\d{44}/) || [''])[0];\n  if (chaveNoNome && chaveNoNome !== chave) erros.push('a chave lida difere da chave no nome do arquivo');\n}\n\n// ---- campos básicos ----\nif (!d.numero) erros.push('número da nota ausente');\nif (!d.data_emissao) erros.push('data de emissão ausente');\nif (!d.emitente_nome) erros.push('nome do emitente ausente');\nif (!cnpjValido(d.emitente_cnpj)) erros.push('CNPJ do emitente inválido');\nvalidaDestinatario();\nconferirData();\n\n// ---- valores ----\nif (!(n(d.valor_total) > 0)) erros.push('valor total zerado ou ausente');\nif (n(d.qtd_itens) === 0) erros.push('nota sem itens');\nif (Math.abs(n(d.soma_itens) - n(d.valor_produtos)) > 0.05) {\n  erros.push(`soma dos itens (${d.soma_itens}) difere do total de produtos (${d.valor_produtos})`);\n}\n\n// total da nota (vNF) = produtos - desconto - ICMS desonerado + ST + FCP-ST + frete + seguro + outras despesas + II + IPI + IPI devolvido\nif (CONFERIR_VNF && d.totais_completos && n(d.valor_total) > 0) {\n  const esperado = r2(n(d.valor_produtos) - n(d.valor_desconto) - n(d.valor_icms_deson) + n(d.valor_st) + n(d.valor_fcp_st)\n    + n(d.valor_frete) + n(d.valor_seguro) + n(d.valor_outros) + n(d.valor_ii) + n(d.valor_ipi) + n(d.valor_ipi_devol));\n  if (Math.abs(n(d.valor_total) - esperado) > 0.05) {\n    erros.push(`total da nota (${d.valor_total}) não confere com produtos, descontos, frete e impostos (esperado ${esperado})`);\n  }\n}\n\n// PDF lido por regras e reprovado: vale tentar a leitura por IA antes de mandar para revisão\nreturn saida({ reler_com_ia: d.origem === 'pdf_texto' && erros.length > 0 });\n"
      },
      "id": "4f54fbcd-2627-4338-8410-c6b0c9a8756a",
      "name": "Validar NF-e",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1136,
        544
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Normaliza a resposta do Gemini (node \"Analyze document\") para o mesmo formato do caminho XML\nconst r = $input.item.json;\n\n// o node do n8n pode devolver o formato simplificado ({content:{parts}}) ou o bruto da API ({candidates:[...]})\nconst partes = r.candidates?.[0]?.content?.parts ?? r.content?.parts ?? [];\nlet texto = partes.filter(p => p.text && !p.thought).map(p => p.text).join('');\nif (!texto) texto = r.text ?? r.output ?? r.response ?? '';\nif (!texto) {\n  throw new Error(\n    'Resposta vazia do Gemini. Campos recebidos: ' + Object.keys(r).join(', ') +\n    ' | início: ' + JSON.stringify(r).slice(0, 300)\n  );\n}\n\n// remove cercas ```json e pega só o trecho entre a primeira { e a última }\nconst limpo = String(texto).replace(/```json|```/gi, '').trim();\nlet extracted;\ntry {\n  extracted = JSON.parse(limpo.slice(limpo.indexOf('{'), limpo.lastIndexOf('}') + 1));\n} catch (e) {\n  throw new Error(`JSON inválido na resposta da IA: ${e.message} | início: ${limpo.slice(0, 200)}`);\n}\n\nconst num = v => {\n  const n = parseFloat(v);\n  return isNaN(n) ? 0 : Math.round(n * 100) / 100;\n};\nconst digitos = v => String(v || '').replace(/\\D/g, '');\n// tipo do documento declarado pela IA (vazio = o validador deduz pela chave)\nconst tipoIA = v => {\n  const s = String(v || '').toUpperCase().replace(/[^A-Z0-9]/g, '');\n  if (/^NFPS/.test(s)) return 'NFPS-e';\n  if (/^NFS/.test(s)) return 'NFS-e';\n  if (s === 'NFCE') return 'NFC-e';\n  if (/^CTE/.test(s)) return 'CT-e';\n  if (/^MDFE/.test(s)) return 'MDF-e';\n  if (s === 'NFE' || s === 'DANFE') return 'NF-e';\n  return '';\n};\n// aceita AAAA-MM-DD ou DD/MM/AAAA\nconst dataIso = v => {\n  const s = String(v || '').trim();\n  const br = /^(\\d{2})\\/(\\d{2})\\/(\\d{4})/.exec(s);\n  return br ? `${br[3]}-${br[2]}-${br[1]}` : s.slice(0, 10);\n};\n\nconst itens = (Array.isArray(extracted.itens) ? extracted.itens : []).map(i => ({\n  codigo: String(i.codigo || '').trim(),\n  descricao: String(i.descricao || '').trim(),\n  ncm: digitos(i.ncm),\n  quantidade: num(i.quantidade),\n  valor_unitario: num(i.valor_unitario),\n  valor_total: num(i.valor_total),\n}));\n\nconst parcelas = (Array.isArray(extracted.parcelas) ? extracted.parcelas : [])\n  .map(p => ({ numero: '', vencimento: dataIso(p.vencimento), valor: num(p.valor) }))\n  .filter(p => /^\\d{4}-\\d{2}-\\d{2}$/.test(p.vencimento));\n\n// a soma é SEMPRE calculada a partir dos itens (não confiar na soma informada pela IA,\n// senão a validação \"itens x total\" nunca pegaria um erro de leitura)\nconst somaCalculada = Math.round(itens.reduce((s, i) => s + i.valor_total, 0) * 100) / 100;\n\nconst CAMPOS_TOTAL = ['valor_frete', 'valor_seguro', 'valor_desconto', 'valor_outros', 'valor_st', 'valor_ipi'];\nconst temTotais = CAMPOS_TOTAL.every(k => extracted[k] !== undefined && extracted[k] !== null && extracted[k] !== '');\n\nreturn {\n  json: {\n    tipo_documento: tipoIA(extracted.tipo_documento),\n    chave_acesso: digitos(extracted.chave_acesso),\n    numero: String(extracted.numero || '').trim(),\n    serie: String(extracted.serie || '').trim(),\n    data_emissao: dataIso(extracted.data_emissao),\n    natureza_operacao: String(extracted.natureza_operacao || '').trim(),\n    emitente_cnpj: digitos(extracted.emitente_cnpj),\n    emitente_nome: String(extracted.emitente_nome || '').trim(),\n    destinatario_doc: digitos(extracted.destinatario_doc),\n    destinatario_nome: String(extracted.destinatario_nome || '').trim(),\n    valor_produtos: num(extracted.valor_produtos || extracted.valor_total),\n    valor_icms: num(extracted.valor_icms),\n    valor_total: num(extracted.valor_total),\n    valor_frete: num(extracted.valor_frete),\n    valor_seguro: num(extracted.valor_seguro),\n    valor_desconto: num(extracted.valor_desconto),\n    valor_outros: num(extracted.valor_outros),\n    valor_st: num(extracted.valor_st),\n    valor_ipi: num(extracted.valor_ipi),\n    totais_completos: temTotais && num(extracted.valor_total) > 0,   // liga a conferência do total (vNF) também para leitura por IA\n    qtd_itens: itens.length,\n    soma_itens: somaCalculada,\n    itens: JSON.stringify(itens),\n    parcelas: JSON.stringify(parcelas),\n    observacoes_ia: String(extracted.observacoes_ia || '').trim(),\n    origem: 'ia_vision',\n    processado_em: new Date().toISOString(),\n  },\n};\n"
      },
      "id": "2be14465-7f94-4277-8ada-21ca6305f273",
      "name": "Normalizar IA",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        832
      ]
    },
    {
      "parameters": {
        "modelName": "models/gemini-3.5-flash-lite",
        "options": {}
      },
      "id": "e53eb4ed-d9a9-4238-bc43-8b394d2c1ce1",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1.1,
      "position": [
        480,
        1056
      ],
      "credentials": {
        "googlePalmApi": {
          "id": "qniydn4cR7qY7Ows",
          "name": "Google Gemini(PaLM) Api account"
        }
      }
    },
    {
      "parameters": {
        "model": "mistral-small-latest",
        "options": {}
      },
      "id": "0a69619b-4328-4add-bd7c-180b4402c985",
      "name": "Mistral Cloud Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatMistralCloud",
      "typeVersion": 1,
      "position": [
        688,
        1056
      ],
      "credentials": {
        "mistralCloudApi": {
          "id": "Bk1yuOLUxyagplZ9",
          "name": "Mistral Cloud account"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Extraia os dados da nota fiscal.\nArquivo: {{ $('Detectar Formato').item.json.nome_arquivo }}\n{{ $json.texto_pdf ? 'Texto extraído do PDF (use SOMENTE este texto):\\n' + $json.texto_pdf : 'O documento está anexado como imagem.' }}",
        "needsFallback": true,
        "options": {
          "systemMessage": "Você é um auditor fiscal especialista em extração de dados de Notas Fiscais (NF-e, DANFE, NFS-e, NFPS-e), faturas e recibos. Extraia os dados do documento com máxima fidelidade.\n\nComo obter o conteúdo do documento:\n- Se a mensagem trouxer \"Texto extraído do PDF\", extraia os dados APENAS desse texto.\n- Se o documento for uma IMAGEM, ela vem anexada à mensagem: leia diretamente.\n- Se não houver texto nem imagem legível, NÃO invente nada: devolva todos os campos vazios (texto \"\" e número 0), itens [] e explique em observacoes_ia. Nunca use valores de exemplo ou placeholders.\n\nResponda SOMENTE com um objeto JSON válido, sem texto antes ou depois e sem markdown, exatamente neste formato:\n\n{\n  \"tipo_documento\": \"NF-e, NFC-e, CT-e, NFS-e, NFPS-e ou outro\",\n  \"chave_acesso\": \"44 dígitos, sem espaços (vazio se o documento não tiver)\",\n  \"numero\": \"número da nota\",\n  \"serie\": \"série\",\n  \"data_emissao\": \"AAAA-MM-DD\",\n  \"natureza_operacao\": \"texto\",\n  \"emitente_cnpj\": \"somente números\",\n  \"emitente_nome\": \"razão social do emitente\",\n  \"destinatario_doc\": \"CNPJ ou CPF do destinatário, somente números\",\n  \"destinatario_nome\": \"nome do destinatário\",\n  \"valor_produtos\": 0.00,\n  \"valor_icms\": 0.00,\n  \"valor_frete\": 0.00,\n  \"valor_seguro\": 0.00,\n  \"valor_desconto\": 0.00,\n  \"valor_outros\": 0.00,\n  \"valor_st\": 0.00,\n  \"valor_ipi\": 0.00,\n  \"valor_total\": 0.00,\n  \"parcelas\": [\n    { \"vencimento\": \"AAAA-MM-DD\", \"valor\": 0.00 }\n  ],\n  \"itens\": [\n    {\n      \"codigo\": \"código do produto\",\n      \"descricao\": \"descrição\",\n      \"ncm\": \"somente números\",\n      \"quantidade\": 0.0,\n      \"valor_unitario\": 0.00,\n      \"valor_total\": 0.00\n    }\n  ],\n  \"observacoes_ia\": \"avisos sobre campos ilegíveis ou incertos (ou vazio)\"\n}\n\nRegras:\n1. Valores numéricos em decimal puro com ponto (exemplo: 1500.50), sem \"R$\" nem separador de milhar.\n2. Se um campo não estiver visível ou legível, use \"\" (texto) ou 0 (número). Nunca invente valores.\n3. Liste TODOS os itens do documento, na ordem em que aparecem.\n4. A chave de acesso só deve ser preenchida se estiver impressa no documento; confira que tem exatamente 44 dígitos.\n5. NFS-e e NFPS-e (nota de serviço): tipo_documento = \"NFS-e\" (ou \"NFPS-e\" se o documento for a Nota Fiscal Avulsa/Prestador Simplificado de Florianópolis); \"numero\" é o NÚMERO DA NOTA, nunca o código de verificação, protocolo, RPS ou número do processo; emitente é o PRESTADOR do serviço e destinatário é o TOMADOR; se não houver itens com NCM, devolva um único item com a descrição do serviço prestado e o valor do serviço.\n6. CPF, CNPJ, chave de acesso, número e série: copie exatamente como estão impressos. Nunca complete, corrija nem ajuste dígitos, mesmo que pareçam inválidos. Se algum dígito estiver ilegível, deixe o campo vazio e explique em observacoes_ia.\n7. valor_icms é o total do campo \"VALOR DO ICMS\" do cálculo do imposto da nota (não o ICMS de um item, nem base de cálculo, nem alíquota). Se não houver, 0.\n8. parcelas: vencimentos e valores da fatura/duplicata, um por parcela. Se a nota não tiver, devolva [].\n9. valor_total é o VALOR TOTAL DA NOTA (já com frete, descontos e impostos), nunca o total dos produtos. valor_produtos é o total dos produtos. Preencha frete, seguro, desconto, outras despesas, ICMS ST e IPI com os valores do quadro de cálculo do imposto (0 se não houver).",
          "maxIterations": 6,
          "passthroughBinaryImages": true
        }
      },
      "id": "10ded929-1655-4804-96ea-d09531d521f0",
      "name": "Analyze document1",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2.2,
      "position": [
        560,
        832
      ],
      "retryOnFail": true,
      "waitBetweenTries": 4000
    },
    {
      "parameters": {
        "amount": 1,
        "unit": "minutes"
      },
      "id": "e879fccc-f67c-43f8-a00b-aadd8997b0e4",
      "name": "Wait1",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        336,
        832
      ],
      "webhookId": "ba1cd557-2476-4332-b009-a03f3a4c2b61"
    },
    {
      "parameters": {
        "amount": 1,
        "unit": "minutes"
      },
      "id": "734b954f-5fb4-4068-b126-e8c7f1e6f57d",
      "name": "Wait2",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1088,
        1088
      ],
      "webhookId": "85bcd539-b0fe-4b72-bc94-c324a679b926"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Lê o texto de um DANFE (PDF com camada de texto) por regras, sem IA.\n// Devolve o mesmo formato do caminho XML. Se algo não for encontrado, o campo fica vazio\n// e a validação posterior encaminha a nota para a leitura por IA.\nconst j = $input.item.json;\nconst bruto = j.text ?? j.data?.text ?? (typeof j.data === 'string' ? j.data : '') ?? '';\nconst F = String(bruto).replace(/\\s+/g, ' ').trim();   // tudo em uma linha: não depende da ordem das quebras\n\nconst pega = (re, grupo = 1) => { const m = re.exec(F); return m ? m[grupo].trim() : ''; };\nconst digitos = s => String(s || '').replace(/\\D/g, '');\n\n// aceita 1.234,56 | 1234,56 | 1234.56 | 5350.00\nconst numero = (s, casas = 2) => {\n  s = String(s || '').trim().replace(/[^\\d.,-]/g, '');\n  if (!s) return 0;\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else if (s.includes(',')) {\n    s = s.replace(',', '.');\n  } else if (/^\\d{1,3}(\\.\\d{3})+$/.test(s)) {\n    s = s.replace(/\\./g, '');\n  }\n  const n = parseFloat(s);\n  const f = 10 ** casas;\n  return isNaN(n) ? 0 : Math.round(n * f) / f;\n};\n\nconst dataIso = s => {\n  const br = /^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/.exec(s);\n  return br ? `${br[3]}-${br[2]}-${br[1]}` : s;\n};\n\n// ---- cabeçalho ----\nconst chave = digitos(pega(/CHAVE DE ACESSO\\s+((?:\\d{4}\\s){10}\\d{4})/) || pega(/((?:\\d{4}\\s){10}\\d{4})/));\nconst numeroNota = digitos(pega(/N[ºo°]\\s*([\\d.]{5,})/));\nconst emitenteNome = pega(/RECEBEMOS DE (.+?) OS PRODUTOS/);\nconst emitenteCnpj = digitos(pega(/CNPJ DO EMITENTE\\s+([\\d./-]{14,18})/) || pega(/CNPJ:\\s*([\\d./-]{14,18})/));\nconst destNome = pega(/NOME \\/ RAZ[ÃA]O SOCIAL\\s+(.+?)\\s+CNPJ \\/ CPF/);\nconst destDoc = digitos(pega(/CNPJ \\/ CPF\\s+([\\d./-]{11,18})/));\n\n// ---- totais ----\nconst vProdutos = numero(pega(/VALOR TOTAL DOS PRODUTOS\\s+R\\$\\s*([\\d.,]+)/));\nconst vNota = numero(pega(/VALOR TOTAL DA NOTA\\s+R\\$\\s*([\\d.,]+)/));\nconst vIcms = numero(pega(/VALOR DO ICMS(?!\\s*S\\.T)\\s+R\\$\\s*([\\d.,]+)/));\nconst vFrete = numero(pega(/VALOR DO FRETE\\s+R\\$\\s*([\\d.,]+)/));\nconst vSeguro = numero(pega(/VALOR DO SEGURO\\s+R\\$\\s*([\\d.,]+)/));\nconst vDesconto = numero(pega(/DESCONTO\\s+R\\$\\s*([\\d.,]+)/));\nconst vOutros = numero(pega(/OUTRAS DESPESAS\\s+R\\$\\s*([\\d.,]+)/));\nconst vSt = numero(pega(/VALOR DO ICMS S\\.T\\.\\s+R\\$\\s*([\\d.,]+)/));\n// só confere o total da nota se o DANFE trouxe todos os componentes\nconst temTotais = ['VALOR DO FRETE', 'VALOR DO SEGURO', 'DESCONTO', 'OUTRAS DESPESAS', 'VALOR TOTAL DA NOTA'].every(l => F.includes(l));\n\n// ---- itens ----\n// cada item termina em: NCM(8) CST CFOP(4) UN QTD V.UNIT V.TOTAL [BC ICMS ALIQ opcionais]\nconst inicio = /ALIQ\\s*%/.exec(F);\nconst fim = F.indexOf('DADOS ADICIONAIS');\nconst trecho = inicio ? F.slice(inicio.index + inicio[0].length, fim > 0 ? fim : undefined) : '';\nconst N = '\\\\d[\\\\d.,]*';\nconst reItem = new RegExp(\n  `(\\\\S+)\\\\s+(.+?)\\\\s+(\\\\d{8})\\\\s+(\\\\d{2,3})\\\\s+(\\\\d{4})\\\\s+([A-Z]{1,5}(?:\\\\s[A-Z])?)\\\\s+(${N})\\\\s+(${N})\\\\s+(${N})` +\n  `(?:\\\\s+${N}(?:\\\\s\\\\d(?=\\\\s|$))?){0,3}`, 'g'\n);\nlet itens = [];\nfor (const m of trecho.matchAll(reItem)) {\n  itens.push({\n    codigo: m[1],\n    descricao: m[2].trim(),\n    ncm: m[3],\n    quantidade: numero(m[7], 4),\n    valor_unitario: numero(m[8], 4),\n    valor_total: numero(m[9]),\n  });\n}\n// confere quantidade x unitário = total; se algum item não fecha, a leitura por regras não é confiável\nconst itensOk = itens.every(i => Math.abs(i.quantidade * i.valor_unitario - i.valor_total) <= 0.02);\nif (!itensOk) itens = [];\n\nconst soma = Math.round(itens.reduce((s, i) => s + i.valor_total, 0) * 100) / 100;\n\n// vencimentos: bloco FATURA/DUPLICATA do DANFE (\"Venc. 15/11/2026 Valor R$ 1.500,00\"). Se não achar, fica vazio\nconst parcelas = [];\nfor (const m of F.matchAll(/Venc\\.?\\s*:?\\s*(\\d{2}\\/\\d{2}\\/\\d{4})\\s+Valor\\s*:?\\s*(?:R\\$)?\\s*([\\d.,]+)/gi)) {\n  parcelas.push({ numero: '', vencimento: dataIso(m[1]), valor: numero(m[2]) });\n}\n\nreturn {\n  json: {\n    chave_acesso: chave,\n    numero: numeroNota ? String(Number(numeroNota)) : '',\n    serie: pega(/S[ÉE]RIE:\\s*(\\d+)/),\n    data_emissao: dataIso(pega(/DATA DA EMISS[ÃA]O\\s+(\\d{4}-\\d{2}-\\d{2}|\\d{2}\\/\\d{2}\\/\\d{4})/)),\n    natureza_operacao: pega(/NATUREZA DA OPERA[ÇC][ÃA]O\\s+(.+?)\\s+PROTOCOLO/),\n    emitente_cnpj: emitenteCnpj,\n    emitente_nome: emitenteNome,\n    destinatario_doc: destDoc,\n    destinatario_nome: destNome,\n    valor_produtos: vProdutos,\n    valor_icms: vIcms,\n    valor_total: vNota,\n    valor_frete: vFrete,\n    valor_seguro: vSeguro,\n    valor_desconto: vDesconto,\n    valor_outros: vOutros,\n    valor_st: vSt,\n    totais_completos: temTotais && vNota > 0,\n    qtd_itens: itens.length,\n    soma_itens: soma,\n    itens: JSON.stringify(itens),\n    parcelas: JSON.stringify(parcelas),\n    origem: 'pdf_texto',\n    texto_pdf: F.slice(0, 12000),\n    processado_em: new Date().toISOString(),\n  },\n  // mantém o arquivo original para o caso de precisar enviar ao Gemini\n  binary: $('Detectar Formato').item.binary,\n};\n"
      },
      "id": "ae9e80fd-0359-42ee-a55e-31bc944b8fbe",
      "name": "Ler DANFE (regras)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        544
      ]
    },
    {
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "file",
        "options": {}
      },
      "id": "5804ed83-2e51-4ef1-a113-1a0137514212",
      "name": "Extrair texto do PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        336,
        544
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Normaliza o XML (já convertido em JSON) para um registro plano e identifica o TIPO do documento\nconst raw = $input.item.json.data || $input.item.json;\n\nconst un = v => Array.isArray(v) ? un(v[0]) : v;\nconst txt = v => {\n  v = un(v);\n  if (v && typeof v === 'object') v = v._ ?? v['#text'] ?? '';\n  return v == null ? '' : String(v).trim();\n};\nconst num = v => { const n = parseFloat(txt(v)); return isNaN(n) ? 0 : n; };\n// número em formato brasileiro (1.234,56) ou com ponto (1234.56)\nconst numBR = v => {\n  let s = txt(v).replace(/[^\\d.,-]/g, '');\n  if (!s) return 0;\n  if (s.includes(',')) s = s.replace(/\\./g, '').replace(',', '.');\n  const n = parseFloat(s);\n  return isNaN(n) ? 0 : n;\n};\nconst toArr = v => v == null ? [] : (Array.isArray(v) ? v : [v]);\nconst digitos = s => String(s || '').replace(/\\D/g, '');\n\n// busca recursiva: devolve o primeiro valor preenchido cuja chave seja um dos nomes (na ordem dada, nível mais raso primeiro)\nconst achar = (obj, nomes, prof = 0) => {\n  if (obj == null || typeof obj !== 'object' || prof > 12) return undefined;\n  if (Array.isArray(obj)) {\n    for (const x of obj) { const r = achar(x, nomes, prof + 1); if (r !== undefined) return r; }\n    return undefined;\n  }\n  for (const nome of nomes) {\n    for (const k of Object.keys(obj)) {\n      if (k.toLowerCase() === nome.toLowerCase() && obj[k] != null && txt(obj[k]) !== '') return obj[k];\n    }\n  }\n  for (const k of Object.keys(obj)) { const r = achar(obj[k], nomes, prof + 1); if (r !== undefined) return r; }\n  return undefined;\n};\n// igual ao achar, mas casa a chave por expressão regular (plano B quando o nome exato não é conhecido)\nconst acharRe = (obj, re, prof = 0) => {\n  if (obj == null || typeof obj !== 'object' || prof > 12) return undefined;\n  if (Array.isArray(obj)) {\n    for (const x of obj) { const r = acharRe(x, re, prof + 1); if (r !== undefined) return r; }\n    return undefined;\n  }\n  for (const k of Object.keys(obj)) { if (re.test(k) && obj[k] != null && txt(obj[k]) !== '') return obj[k]; }\n  for (const k of Object.keys(obj)) { const r = acharRe(obj[k], re, prof + 1); if (r !== undefined) return r; }\n  return undefined;\n};\n// primeiro sub-objeto cuja chave casa com a expressão (ex.: o bloco do prestador)\nconst subarvore = (obj, re, prof = 0) => {\n  if (obj == null || typeof obj !== 'object' || prof > 12) return undefined;\n  if (Array.isArray(obj)) {\n    for (const x of obj) { const r = subarvore(x, re, prof + 1); if (r !== undefined) return r; }\n    return undefined;\n  }\n  for (const k of Object.keys(obj)) { if (re.test(k) && obj[k] && typeof un(obj[k]) === 'object') return un(obj[k]); }\n  for (const k of Object.keys(obj)) { const r = subarvore(obj[k], re, prof + 1); if (r !== undefined) return r; }\n  return undefined;\n};\nconst dataIso = s => {\n  s = String(s || '').trim();\n  const br = /^(\\d{2})\\/(\\d{2})\\/(\\d{4})/.exec(s);\n  return br ? `${br[3]}-${br[2]}-${br[1]}` : s.slice(0, 10);\n};\n\n// ---- tipo de documento, pela estrutura do XML ----\nlet tipo = 'desconhecido';\nif (raw.nfeProc || raw.NFe) tipo = 'NF-e';\nelse if (raw.cteProc || raw.CTe) tipo = 'CT-e';\nelse if (raw.mdfeProc || raw.MDFe) tipo = 'MDF-e';\nelse if (raw.xmlNfpse || /\"[^\"]*nfpse[^\"]*\"\\s*:/i.test(JSON.stringify(raw))) tipo = 'NFPS-e';   // nota fiscal de serviço de Florianópolis (raiz <xmlNfpse>)\nelse if (/\"[^\"]*nfse[^\"]*\"\\s*:/i.test(JSON.stringify(raw))) tipo = 'NFS-e';   // qualquer padrão municipal ou o nacional\n\n// documentos que ainda não lemos: devolve um registro vazio com o tipo, em vez de dar erro no workflow\nconst registroVazio = (tipoDoc) => {\n  const m = JSON.stringify(raw).match(/\"(?:Id|chCTe|chMDFe)\":\"(?:CTe|MDFe)?(\\d{44})\"/);\n  return { json: {\n    tipo_documento: tipoDoc, chave_acesso: m ? m[1] : '', numero: '', serie: '', data_emissao: '',\n    natureza_operacao: '', emitente_cnpj: '', emitente_nome: '', destinatario_doc: '', destinatario_nome: '',\n    valor_produtos: 0, valor_icms: 0, valor_total: 0, qtd_itens: 0, soma_itens: 0, itens: '[]',\n    origem: 'xml', processado_em: new Date().toISOString(),\n  } };\n};\n\n// ---- NFPS-e (Florianópolis): campos próprios, nomes conhecidos primeiro e plano B por expressão ----\nif (tipo === 'NFPS-e') {\n  const valor = numBR(achar(raw, ['valorTotalServicos', 'valorServicos', 'valorLiquido']));\n  const desc = txt(achar(raw, ['discriminacaoServico', 'discriminacao', 'descricaoServico', 'descricao']) ?? acharRe(raw, /discrimin|descri.*serv/i));\n  const dataBruta = txt(\n    achar(raw, ['dataEmissao', 'dataHoraEmissao', 'dtEmissao', 'dataEmissaoNfpse', 'dataFatoGerador', 'dataCompetencia'])\n    ?? acharRe(raw, /(data|dt|dh).*(emiss|fato|competen)|emiss.*(data|dt|dh)/i)\n  );\n  // identificacaoTomador pode ser um bloco (com CNPJ/CPF e nome) ou só o número do documento\n  const tomObj = subarvore(raw, /tomador/i);\n  const tomDoc = tomObj\n    ? achar(tomObj, ['cnpj', 'cpf', 'cpfCnpj', 'cnpjCpf', 'documento', 'numeroDocumento'])\n    : achar(raw, ['identificacaoTomador']);\n  const tomNome = achar(tomObj || {}, ['razaoSocial', 'razaoSocialTomador', 'nomeRazaoSocial', 'nome'])\n    ?? achar(raw, ['razaoSocialTomador', 'nomeTomador', 'nomeRazaoSocialTomador']);\n  const cnpjPrest = achar(raw, ['cnpjPrestador']) ?? acharRe(raw, /cnpj.*prestador|prestador.*cnpj/i);\n  const itens = valor > 0 ? [{ codigo: '', descricao: (desc || 'Serviço prestado').slice(0, 300), ncm: '', quantidade: 1, valor_unitario: valor, valor_total: valor }] : [];\n  return { json: {\n    tipo_documento: 'NFPS-e',\n    chave_acesso: '',                       // o validador cria a chave sintética (CNPJ + número + data)\n    numero: txt(achar(raw, ['numeroSerie', 'numeroNota', 'numeroNfpse', 'numero'])),   // em NFPS-e, numeroSerie é o número da nota\n    serie: '',\n    data_emissao: dataIso(dataBruta),\n    natureza_operacao: desc.slice(0, 120),\n    emitente_cnpj: digitos(txt(cnpjPrest)),\n    emitente_nome: txt(achar(raw, ['razaoSocialPrestador', 'nomePrestador'])),\n    destinatario_doc: digitos(txt(tomDoc)),\n    destinatario_nome: txt(tomNome),\n    valor_produtos: valor, valor_icms: 0, valor_total: valor,\n    qtd_itens: itens.length, soma_itens: valor, itens: JSON.stringify(itens),\n    origem: 'xml', processado_em: new Date().toISOString(),\n  } };\n}\n\n// ---- NFS-e (serviço): leitura tolerante, vale para ABRASF municipal e para o padrão nacional ----\nif (tipo === 'NFS-e') {\n  const prest = subarvore(raw, /prestador|^emit$/i) || raw;\n  const toma = subarvore(raw, /tomador|^toma$/i) || {};\n  const valor = num(achar(raw, ['vServ', 'ValorServicos', 'vLiq', 'ValorLiquidoNfse']));\n  const desc = txt(achar(raw, ['xDescServ', 'Discriminacao']));\n  const itens = valor > 0 ? [{ codigo: '', descricao: (desc || 'Serviço prestado').slice(0, 300), ncm: '', quantidade: 1, valor_unitario: valor, valor_total: valor }] : [];\n  return { json: {\n    tipo_documento: 'NFS-e',\n    chave_acesso: '',                       // o validador cria a chave sintética (CNPJ + número + data)\n    numero: txt(achar(raw, ['nNFSe', 'Numero', 'NumeroNfse'])),\n    serie: '',\n    data_emissao: dataIso(txt(achar(raw, ['dhEmi', 'DataEmissao', 'dhProc', 'dCompet']))),\n    natureza_operacao: desc.slice(0, 120),\n    emitente_cnpj: digitos(txt(achar(prest, ['CNPJ', 'Cnpj']))),\n    emitente_nome: txt(achar(prest, ['xNome', 'RazaoSocial', 'NomeFantasia'])),\n    destinatario_doc: digitos(txt(achar(toma, ['CNPJ', 'Cnpj', 'CPF', 'Cpf']))),\n    destinatario_nome: txt(achar(toma, ['xNome', 'RazaoSocial'])),\n    valor_produtos: valor, valor_icms: 0, valor_total: valor,\n    qtd_itens: itens.length, soma_itens: valor, itens: JSON.stringify(itens),\n    origem: 'xml', processado_em: new Date().toISOString(),\n  } };\n}\n\nif (tipo !== 'NF-e') return registroVazio(tipo);\n\nconst proc = raw.nfeProc ? un(raw.nfeProc) : raw;\nconst NFe = un(proc.NFe) || {};\nconst inf = un(NFe.infNFe) || {};\nif (Object.keys(inf).length === 0) return registroVazio('desconhecido');\n\n// atributos podem vir em '$', '_attributes' ou mesclados no próprio objeto (mergeAttrs)\nconst attrs = inf.$ || inf._attributes || inf;\nconst ide = un(inf.ide) || {};\nconst emit = un(inf.emit) || {};\nconst dest = un(inf.dest) || {};\nconst tot = un(un(inf.total)?.ICMSTot) || {};\nconst prot = un(un(proc.protNFe)?.infProt) || {};\n\nconst chave = (txt(prot.chNFe) || String(attrs.Id || '').replace(/^NFe/, '')).replace(/\\D/g, '');\n\nconst itens = toArr(inf.det).map(d => {\n  const p = un(d.prod) || {};\n  return {\n    codigo: txt(p.cProd),\n    descricao: txt(p.xProd),\n    ncm: txt(p.NCM),\n    quantidade: num(p.qCom),\n    valor_unitario: num(p.vUnCom),\n    valor_total: num(p.vProd),\n  };\n});\n\nconst soma = Math.round(itens.reduce((s, i) => s + i.valor_total, 0) * 100) / 100;\n// vencimentos (fatura/duplicatas), usados no fluxo de caixa projetado\nconst parcelas = toArr(un(inf.cobr)?.dup)\n  .map(p => ({ numero: txt(p.nDup), vencimento: dataIso(txt(p.dVenc)), valor: num(p.vDup) }))\n  .filter(p => /^\\d{4}-\\d{2}-\\d{2}$/.test(p.vencimento));\nconst dhEmi = txt(ide.dhEmi) || txt(ide.dEmi);\n\nreturn {\n  json: {\n    tipo_documento: txt(ide.mod) === '65' ? 'NFC-e' : 'NF-e',\n    chave_acesso: chave,\n    numero: txt(ide.nNF),\n    serie: txt(ide.serie),\n    data_emissao: dhEmi ? dhEmi.slice(0, 10) : '',\n    natureza_operacao: txt(ide.natOp),\n    emitente_cnpj: txt(emit.CNPJ),\n    emitente_nome: txt(emit.xNome),\n    destinatario_doc: txt(dest.CNPJ) || txt(dest.CPF),\n    destinatario_nome: txt(dest.xNome),\n    valor_produtos: num(tot.vProd),\n    valor_icms: num(tot.vICMS),\n    valor_total: num(tot.vNF),\n    // componentes do total (usados para conferir o vNF)\n    valor_frete: num(tot.vFrete),\n    valor_seguro: num(tot.vSeg),\n    valor_desconto: num(tot.vDesc),\n    valor_outros: num(tot.vOutro),\n    valor_st: num(tot.vST),\n    valor_fcp_st: num(tot.vFCPST),\n    valor_ii: num(tot.vII),\n    valor_ipi: num(tot.vIPI),\n    valor_ipi_devol: num(tot.vIPIDevol),\n    valor_icms_deson: num(tot.vICMSDeson),\n    totais_completos: true,\n    qtd_itens: itens.length,\n    soma_itens: soma,\n    itens: JSON.stringify(itens),\n    parcelas: JSON.stringify(parcelas),\n    origem: 'xml',\n    processado_em: new Date().toISOString(),\n  },\n};\n"
      },
      "id": "87f5618b-a6ee-4db4-8c61-eb5a8187da98",
      "name": "Normalizar NF-e",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        784,
        240
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "d0af7518-e767-40e7-a6a4-98bdef02d0fa",
      "name": "Converter XML em JSON",
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [
        560,
        240
      ]
    },
    {
      "parameters": {
        "operation": "xml",
        "binaryPropertyName": "file",
        "options": {}
      },
      "id": "1744fec4-f665-4cf4-8470-60b28b481cf2",
      "name": "Extrair XML",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        336,
        240
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "t-xml",
                    "leftValue": "={{ $json.tipo_arquivo }}",
                    "rightValue": "xml",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "xml"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "t-img",
                    "leftValue": "={{ $json.tipo_arquivo }}",
                    "rightValue": "imagem",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "imagem"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "t-pdf",
                    "leftValue": "={{ $json.tipo_arquivo }}",
                    "rightValue": "pdf",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "pdf"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "não suportado"
        }
      },
      "id": "41eff316-8b37-4077-9401-f4c7ddaa077b",
      "name": "Tipo de arquivo",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        32,
        544
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Atlas Gestão: identifica o formato do arquivo e a empresa cliente a que a nota pertence\nconst CLIENTE_PADRAO_CNPJ = '';   // opcional: CNPJ do cliente quando todas as notas são da mesma empresa\n\nconst item = $input.item;\nconst binary = item.binary?.file || (item.binary ? Object.values(item.binary)[0] : null);\n\nif (!binary) {\n  throw new Error(\"Nenhum anexo encontrado no e-mail.\");\n}\n\nconst mime = (binary.mimeType || '').toLowerCase();\nconst ext = (binary.fileExtension || (binary.fileName || '').split('.').pop() || '').toLowerCase();\n\n// xml -> leitura direta | pdf -> leitura por regras (Gemini só se precisar) | imagem -> Gemini\nlet tipo = 'desconhecido';\nif (/^(text|application)\\/xml/.test(mime) || mime.endsWith('+xml') || ext === 'xml') {   // não usar includes('xml'): .docx/.xlsx têm \"openxmlformats\" no tipo\n  tipo = 'xml';\n} else if (mime.includes('pdf') || ext === 'pdf') {\n  tipo = 'pdf';\n} else if (mime.includes('image') || ['png', 'jpg', 'jpeg', 'webp'].includes(ext)) {\n  tipo = 'imagem';\n}\n\n// CNPJ do cliente, por ordem de prioridade: CNPJ escrito no assunto do e-mail > CLIENTE_PADRAO_CNPJ\nconst digitos = s => String(s || '').replace(/\\D/g, '');\nconst j = item.json || {};\nconst noAssunto = (String(j.subject || '').match(/\\d{2}\\.?\\d{3}\\.?\\d{3}\\/?\\d{4}-?\\d{2}/) || [''])[0];\nconst clienteCnpj = [noAssunto, CLIENTE_PADRAO_CNPJ]\n  .map(digitos).find(x => x.length === 14) || '';\n\nreturn {\n  json: {\n    tipo_arquivo: tipo,\n    nome_arquivo: binary.fileName || 'arquivo_desconhecido',\n    mime_type: binary.mimeType || 'application/octet-stream',\n    tamanho_bytes: Number(binary.bytes) || 0,\n    cliente_cnpj: clienteCnpj,\n    canal: j.canal || 'e-mail'\n  },\n  binary: item.binary\n};\n"
      },
      "id": "b7a5d276-26b9-4db7-9cef-644b77450cf1",
      "name": "Detectar Formato",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -192,
        544
      ]
    },
    {
      "parameters": {
        "content": "## Atlas Gestão: Conferência de NF-e v8 (Esteira Completa)\n\nEsteira automatizada ponta a ponta conforme ofertado pela Atlas Gestão.\n\n**1. Entrada:** E-mail IMAP (separação automática de anexos; CNPJ do cliente pode ir no assunto).\n**2. Extração & Validação:**\n• XML direto | PDF por regras regex | Imagem/PDF complexo via IA Vision (Gemini + Mistral)\n• Validação fiscal completa (chave 44 dígitos, CNPJ/CPF, datas futuras, conferência vNF).\n**3. Classificação & Planilha (Google Sheets):**\n• Classificação por NCM e palavras-chave de serviço.\n• Gravação automática na aba \"Notas\" da planilha mestre.\n**4. Governança & Alçada no Telegram:**\n• **≤ R$ 500 (Auto-aprovação):** Lança na planilha e envia aviso informativo ao sócio.\n• **> R$ 500 (Aprovação Interativa):** Dispara mensagem via Telegram com botões [✅ Aprovar] e [❌ Recusar] (sendAndWait, prazo de 24 h). Se aprovada, grava na planilha; se recusada, não grava e apenas responde.\n\nProjeto conceitual · IdeiaFlui & Atlas Gestão\n\n⚠️ Antes de publicar: desativar o nó Webhook (apenas para testes). Só e-mails de remetentes permitidos (nó 'Separar anexos') são processados.\n\nGravação: por chave de acesso. XML substitui linha lida por PDF/IA; leitura pior nunca substitui melhor. Falhas de gravação (ex.: cota do Google Sheets) avisam no Telegram.",
        "height": 520,
        "width": 780
      },
      "id": "fd1f46c3-4a19-4155-8ee4-8d4afd1f454a",
      "name": "Instruções",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -544,
        -144
      ]
    },
    {
      "parameters": {
        "format": "resolved",
        "options": {}
      },
      "type": "n8n-nodes-base.emailReadImap",
      "typeVersion": 2.2,
      "position": [
        -608,
        544
      ],
      "id": "8f049f73-3030-4ceb-bbfb-d49ef2944f99",
      "name": "E-mail: notas Atlas Gestão",
      "credentials": {
        "imap": {
          "id": "pfOPCCsKkHk1r9If",
          "name": "Email - IdeiaFlui"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Um item por anexo útil (XML, PDF ou imagem). Antes só o primeiro anexo do e-mail era lido.\n// Descarta assinaturas e logos de e-mail (imagens pequenas ou com nome típico).\nconst REMETENTES_PERMITIDOS = ['eduardo.petrocchi@gmail.com'];   // só e-mails destes remetentes são processados\nconst enderecoDe = it => String(it.json.from?.value?.[0]?.address || (String(it.json.from?.text || '').match(/[\\w.+-]+@[\\w.-]+\\.\\w+/) || [''])[0]).toLowerCase();\nconst saida = [];\n$input.all().forEach((item, idx) => {\n  if (!REMETENTES_PERMITIDOS.includes(enderecoDe(item))) return;\n  for (const b of Object.values(item.binary || {})) {\n    const mime = (b.mimeType || '').toLowerCase();\n    const ext = (b.fileExtension || (b.fileName || '').split('.').pop() || '').toLowerCase();\n    const ehXml = /^(text|application)\\/xml/.test(mime) || mime.endsWith('+xml') || ext === 'xml';   // includes('xml') pegaria .docx/.xlsx\n    const ehPdf = mime.includes('pdf') || ext === 'pdf';\n    const ehImg = mime.includes('image') || ['png', 'jpg', 'jpeg', 'webp'].includes(ext);\n    if (!(ehXml || ehPdf || ehImg)) continue;\n    const pequena = Number(b.bytes) > 0 && Number(b.bytes) < 30000;\n    const deAssinatura = /^(image\\d+|logo|outlook|signature|assinatura)/i.test(b.fileName || '');\n    if (ehImg && (pequena || deAssinatura)) continue;\n    saida.push({\n      json: { subject: item.json.subject || '', remetente: item.json.from?.text || '', canal: 'e-mail' },\n      binary: { file: b },\n      pairedItem: { item: idx },\n    });\n  }\n});\nreturn saida;\n"
      },
      "id": "8914c721-776d-402a-a986-b8c44943098e",
      "name": "Separar anexos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -336,
        544
      ]
    },
    {
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "alcada-check",
              "leftValue": "={{ Number($json['Valor total']) || 0 }}",
              "operator": {
                "operation": "lte",
                "type": "number"
              },
              "rightValue": 500
            },
            {
              "id": "origem-xml",
              "leftValue": "={{ $json['Origem'] }}",
              "rightValue": "xml",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "status-ok",
              "leftValue": "={{ $json.status }}",
              "rightValue": "ok",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          }
        },
        "options": {}
      },
      "id": "913577bb-357e-4b0b-a557-2c892cd2b865",
      "name": "Alçada",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2048,
        544
      ]
    },
    {
      "parameters": {
        "chatId": "8858283939",
        "text": "={{ (() => { const e = s => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); const v = Number($json['Valor total']).toLocaleString('pt-BR',{minimumFractionDigits:2}); return '✅ <b>NF-e registrada automaticamente</b>\\n\\n📄 <b>' + e($json['Emitente']) + '</b>\\n🔑 Chave: <code>' + e($json['Chave de acesso']) + '</code>\\n💰 Valor: <b>R$ ' + v + '</b>\\n📅 Emissão: ' + e($json['Mês']) + '\\n📂 Categoria: ' + e($json['Categoria']) + '\\n🔄 Direção: ' + e($json['Direção'] || 'Não identificada') + '\\n\\n✔️ Valor dentro da alçada (≤ R$ 500). Lançada automaticamente na planilha.'; })() }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "81db04ff-f216-457d-b1fb-0ea08a366594",
      "name": "Telegram: Aviso Automático",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2288,
        352
      ],
      "webhookId": "7c9ca760-c80d-4a9f-8b12-f38b1fcacc01",
      "credentials": {
        "telegramApi": {
          "id": "aZpDn65CiuDBOaHs",
          "name": "Atlasgestao"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "sendAndWait",
        "chatId": "8858283939",
        "message": "={{ (() => { const c = s => String(s ?? '').replace(/[_*`\\[\\]]/g,' '); const v = Number($json['Valor total']).toLocaleString('pt-BR',{minimumFractionDigits:2}); const m = []; if (Number($json['Valor total']) > 500) m.push('valor acima da alçada de R$ 500'); if ($json['Origem'] !== 'xml') m.push('lida por IA/PDF: confira os valores no arquivo original'); if ($json.status !== 'ok') m.push('falhou na validação: ' + c($json['Motivo'])); return '⚠️ NF-e para aprovação\\n\\n📄 ' + c($json['Emitente']) + '\\n🔑 Chave: ' + c($json['Chave de acesso']) + '\\n💰 Valor: R$ ' + v + '\\n📅 Emissão: ' + c($json['Mês']) + '\\n📂 Categoria: ' + c($json['Categoria']) + '\\n🔄 Direção: ' + c($json['Direção'] || 'Não identificada') + '\\n📎 Arquivo: ' + c($json['Arquivo']) + '\\n\\nMotivo: ' + m.join('; ') + '\\n\\nAutorize ou recuse o lançamento desta nota.'; })() }}",
        "options": {}
      },
      "id": "bcf1f71e-93db-4f3b-9c89-fc827f4546dd",
      "name": "Telegram: Aguardar Aprovação",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2288,
        736
      ],
      "webhookId": "16188f55-ea06-4dae-aeaf-2d0576c9459d",
      "credentials": {
        "telegramApi": {
          "id": "aZpDn65CiuDBOaHs",
          "name": "Atlasgestao"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI",
          "mode": "list",
          "cachedResultName": "atlas_gestao",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Notas",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Chave de acesso": "={{ $json['Chave de acesso'] }}",
            "Data": "={{ $json['Data'] }}",
            "Mês": "={{ $json['Mês'] }}",
            "Número": "={{ $json['Número'] }}",
            "Série": "={{ $json['Série'] }}",
            "Emitente": "={{ $json['Emitente'] }}",
            "CNPJ do emitente": "={{ $json['CNPJ do emitente'] }}",
            "Destinatário": "={{ $json['Destinatário'] }}",
            "Valor total": "={{ $json['Valor total'] }}",
            "Qtd. itens": "={{ $json['Qtd. itens'] }}",
            "Processado em": "={{ $json['Processado em'] }}",
            "Tipo": "={{ $json['Tipo'] }}",
            "Origem": "={{ $json['Origem'] }}",
            "Categoria": "={{ $json['Categoria'] }}",
            "CNPJ raiz": "={{ $json['CNPJ raiz'] }}",
            "Natureza": "={{ $json['Natureza'] }}",
            "ICMS": "={{ $json['ICMS'] }}",
            "Arquivo": "={{ $json['Arquivo'] }}",
            "Motivo": "={{ $json['Motivo'] }}",
            "Direção": "={{ $json['Direção'] }}",
            "Movimenta caixa": "={{ $json['Movimenta caixa'] }}",
            "Vencimento": "={{ $json['Vencimento'] }}",
            "Ação sugerida": "={{ $json['Ação sugerida'] }}"
          },
          "matchingColumns": [
            "Chave de acesso"
          ],
          "schema": [
            {
              "id": "Chave de acesso",
              "displayName": "Chave de acesso",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Data",
              "displayName": "Data",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Mês",
              "displayName": "Mês",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Número",
              "displayName": "Número",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Série",
              "displayName": "Série",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Emitente",
              "displayName": "Emitente",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CNPJ do emitente",
              "displayName": "CNPJ do emitente",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Destinatário",
              "displayName": "Destinatário",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Valor total",
              "displayName": "Valor total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Qtd. itens",
              "displayName": "Qtd. itens",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Processado em",
              "displayName": "Processado em",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Tipo",
              "displayName": "Tipo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Origem",
              "displayName": "Origem",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Categoria",
              "displayName": "Categoria",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CNPJ raiz",
              "displayName": "CNPJ raiz",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Natureza",
              "displayName": "Natureza",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ICMS",
              "displayName": "ICMS",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Arquivo",
              "displayName": "Arquivo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Motivo",
              "displayName": "Motivo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Direção",
              "displayName": "Direção",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Movimenta caixa",
              "displayName": "Movimenta caixa",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Vencimento",
              "displayName": "Vencimento",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Ação sugerida",
              "displayName": "Ação sugerida",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "fc136a4a-7ae9-48d3-8b02-41b2c4c1b432",
      "name": "Gravar na Planilha",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2528,
        352
      ],
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 5000,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "97Zp6EpnZyWprYf0",
          "name": "Google Sheets - IdeiaFlui"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI",
          "mode": "list",
          "cachedResultName": "atlas_gestao",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Notas",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Chave de acesso": "={{ $json['Chave de acesso'] }}",
            "Data": "={{ $json['Data'] }}",
            "Mês": "={{ $json['Mês'] }}",
            "Número": "={{ $json['Número'] }}",
            "Série": "={{ $json['Série'] }}",
            "Emitente": "={{ $json['Emitente'] }}",
            "CNPJ do emitente": "={{ $json['CNPJ do emitente'] }}",
            "Destinatário": "={{ $json['Destinatário'] }}",
            "Valor total": "={{ $json['Valor total'] }}",
            "Qtd. itens": "={{ $json['Qtd. itens'] }}",
            "Processado em": "={{ $json['Processado em'] }}",
            "Tipo": "={{ $json['Tipo'] }}",
            "Origem": "={{ $json['Origem'] }}",
            "Categoria": "={{ $json['Categoria'] }}",
            "CNPJ raiz": "={{ $json['CNPJ raiz'] }}",
            "Natureza": "={{ $json['Natureza'] }}",
            "ICMS": "={{ $json['ICMS'] }}",
            "Arquivo": "={{ $json['Arquivo'] }}",
            "Motivo": "={{ $json['Motivo'] }}",
            "Direção": "={{ $json['Direção'] }}",
            "Movimenta caixa": "={{ $json['Movimenta caixa'] }}",
            "Vencimento": "={{ $json['Vencimento'] }}",
            "Ação sugerida": "={{ $json['Ação sugerida'] }}"
          },
          "matchingColumns": [
            "Chave de acesso"
          ],
          "schema": [
            {
              "id": "Chave de acesso",
              "displayName": "Chave de acesso",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Data",
              "displayName": "Data",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Mês",
              "displayName": "Mês",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Número",
              "displayName": "Número",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Série",
              "displayName": "Série",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Emitente",
              "displayName": "Emitente",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CNPJ do emitente",
              "displayName": "CNPJ do emitente",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Destinatário",
              "displayName": "Destinatário",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Valor total",
              "displayName": "Valor total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Qtd. itens",
              "displayName": "Qtd. itens",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Processado em",
              "displayName": "Processado em",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Tipo",
              "displayName": "Tipo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Origem",
              "displayName": "Origem",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Categoria",
              "displayName": "Categoria",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CNPJ raiz",
              "displayName": "CNPJ raiz",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Natureza",
              "displayName": "Natureza",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ICMS",
              "displayName": "ICMS",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Arquivo",
              "displayName": "Arquivo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Motivo",
              "displayName": "Motivo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Direção",
              "displayName": "Direção",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Movimenta caixa",
              "displayName": "Movimenta caixa",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Vencimento",
              "displayName": "Vencimento",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Ação sugerida",
              "displayName": "Ação sugerida",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "db1f1787-6391-4261-8277-77721ecb2da6",
      "name": "Gravar na Planilha (Aprovada)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2816,
        752
      ],
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 5000,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "97Zp6EpnZyWprYf0",
          "name": "Google Sheets - IdeiaFlui"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Recupera todos os dados fiscais e contábeis da nota calculados em 'Montar linhas'\nconst dados = $('Filtrar duplicadas').item.json;\nconst decisao = $input.item.json.data || $input.item.json || {};\nconst aprovado = decisao.approved === true;\n\nreturn {\n  json: {\n    ...dados,\n    'Status aprovação': aprovado ? 'Aprovado pelo sócio (Telegram)' : 'Recusado pelo sócio (Telegram)',\n    'Respondido em': decisao.respondedAt || new Date().toISOString(),\n    'Decisão sócio': aprovado ? 'Aprovado' : 'Recusado',\n    status: aprovado ? dados.status : 'recusado',\n    mensagem: aprovado\n      ? `Nota ${dados.Número} APROVADA pelo sócio via Telegram: ${dados.Emitente}, R$ ${dados['Valor total']}. Lançamento efetivado.`\n      : `Nota ${dados.Número} RECUSADA pelo sócio via Telegram: ${dados.Emitente}, R$ ${dados['Valor total']}. Não autorizada.`\n  }\n};"
      },
      "id": "cbeb2151-8102-4a68-a376-5207f2836157",
      "name": "Processar Decisão",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        832
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "aprovada-check",
              "leftValue": "={{ $json['Decisão sócio'] }}",
              "rightValue": "Aprovado",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2592,
        832
      ],
      "id": "91ee439d-408e-4c91-9dc0-3a47c9c568cd",
      "name": "Aprovada?"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        336,
        1296
      ],
      "id": "4d282306-2f3b-484b-b806-84d764b4251b",
      "name": "Formato não suportado"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI",
          "mode": "list",
          "cachedResultName": "atlas_gestao",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Notas",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bzFyJy7n-kR0To-t82VtST5fW8hvv03KNEOywlFcolI/edit#gid=0"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1808,
        336
      ],
      "id": "a8d9208d-858e-4502-9b72-07115d6feb32",
      "name": "Ler chaves existentes",
      "alwaysOutputData": true,
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 5000,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "97Zp6EpnZyWprYf0",
          "name": "Google Sheets - IdeiaFlui"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Descarta notas já registradas, EXCETO quando a nova leitura é mais confiável que a gravada (xml > pdf_texto > ia_vision): nesse caso ela passa e substitui a linha (gravação por chave de acesso).\n// Também descarta duplicatas dentro do mesmo lote, mantendo a melhor leitura.\nconst PRIORIDADE = { xml: 0, pdf_texto: 1, ia_vision: 2 };\nconst pri = o => PRIORIDADE[o] ?? 9;\nconst existentes = new Map();\nfor (const i of $('Ler chaves existentes').all()) {\n  const k = String(i.json['Chave de acesso'] || '').trim();\n  if (k) existentes.set(k, i.json['Origem']);\n}\nconst melhor = new Map();\nconst sobra = [];\nfor (const it of $('Montar linhas').all()) {\n  const chave = String(it.json['Chave de acesso'] || '').trim();\n  if (!chave) { sobra.push(it); continue; }\n  if (existentes.has(chave)) {\n    const gravada = existentes.get(chave);\n    // origem desconhecida na linha gravada: mantém a linha. Só substitui se a nova leitura for estritamente melhor\n    if (!(gravada in PRIORIDADE) || pri(it.json['Origem']) >= pri(gravada)) continue;\n  }\n  const atual = melhor.get(chave);\n  if (!atual || pri(it.json['Origem']) < pri(atual.json['Origem'])) melhor.set(chave, it);\n}\nreturn [...melhor.values(), ...sobra].map(it => ({ json: it.json, pairedItem: { item: 0 } }));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1968,
        336
      ],
      "id": "c9a22d61-8822-4929-ac98-fadad1977209",
      "name": "Filtrar duplicadas"
    },
    {
      "parameters": {
        "chatId": "8858283939",
        "text": "={{ '❌ Falha ao gravar NF-e na planilha\\n\\n📎 Arquivo: ' + ($json['Arquivo'] || '(desconhecido)') + '\\n📄 Emitente: ' + ($json['Emitente'] || '-') + '\\n🔑 Chave: ' + ($json['Chave de acesso'] || '-') + '\\n\\nErro: ' + String($json.error?.message || $json.error || 'não informado').slice(0, 300) + '\\n\\nA nota NÃO foi registrada. Reenvie o arquivo.' }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2848,
        496
      ],
      "id": "89587a49-97a6-4de4-999e-de4f0b2b345a",
      "name": "Telegram: Falha na gravação",
      "webhookId": "d2e3f4a5-4444-4c3d-9e4f-6a7b8c9d0e04",
      "credentials": {
        "telegramApi": {
          "id": "aZpDn65CiuDBOaHs",
          "name": "Atlasgestao"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "Montar linhas": {
      "main": [
        [
          {
            "node": "Ler chaves existentes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reler com IA?": {
      "main": [
        [
          {
            "node": "Wait2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Montar linhas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar NF-e": {
      "main": [
        [
          {
            "node": "Reler com IA?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalizar IA": {
      "main": [
        [
          {
            "node": "Validar NF-e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze document1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Mistral Cloud Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze document1",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    },
    "Analyze document1": {
      "main": [
        [
          {
            "node": "Normalizar IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait1": {
      "main": [
        [
          {
            "node": "Analyze document1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait2": {
      "main": [
        [
          {
            "node": "Analyze document1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ler DANFE (regras)": {
      "main": [
        [
          {
            "node": "Validar NF-e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair texto do PDF": {
      "main": [
        [
          {
            "node": "Ler DANFE (regras)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalizar NF-e": {
      "main": [
        [
          {
            "node": "Validar NF-e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Converter XML em JSON": {
      "main": [
        [
          {
            "node": "Normalizar NF-e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair XML": {
      "main": [
        [
          {
            "node": "Converter XML em JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tipo de arquivo": {
      "main": [
        [
          {
            "node": "Extrair XML",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extrair texto do PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Formato não suportado",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detectar Formato": {
      "main": [
        [
          {
            "node": "Tipo de arquivo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "E-mail: notas Atlas Gestão": {
      "main": [
        [
          {
            "node": "Separar anexos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Separar anexos": {
      "main": [
        [
          {
            "node": "Detectar Formato",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alçada": {
      "main": [
        [
          {
            "node": "Gravar na Planilha",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram: Aguardar Aprovação",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gravar na Planilha": {
      "main": [
        [
          {
            "node": "Telegram: Aviso Automático",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram: Falha na gravação",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: Aguardar Aprovação": {
      "main": [
        [
          {
            "node": "Processar Decisão",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processar Decisão": {
      "main": [
        [
          {
            "node": "Aprovada?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aprovada?": {
      "main": [
        [
          {
            "node": "Gravar na Planilha (Aprovada)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Ler chaves existentes": {
      "main": [
        [
          {
            "node": "Filtrar duplicadas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filtrar duplicadas": {
      "main": [
        [
          {
            "node": "Alçada",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gravar na Planilha (Aprovada)": {
      "main": [
        [],
        [
          {
            "node": "Telegram: Falha na gravação",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": true
  },
  "versionId": "4151ee72-0372-44f9-8738-d5fa5c641723",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "6dce0880725addc9063eb4af7f67bfc819f08daea8e6bcc02b3a33a64aa27983"
  },
  "nodeGroups": [],
  "id": "EvIXyh6N3WL93xx3",
  "tags": []
}