{"updatedAt":"2026-08-17T12:10:27.849Z","createdAt":"2026-08-09T15:26:46.470Z","id":"2QNMnPXfarXJB1Km","name":"Sino Contábil - NF PMO","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"httpMethod":"POST","path":"sino-contabil","responseMode":"onReceived","options":{}},"id":"webhook","name":"Telegram (webhook)","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[-380,300],"webhookId":"sino-contabil-nfpmo"},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst CONFIG = '/home/ultimo/nf-pmo/config.json';\nconst b = $input.first().json.body || {};\nconst cb = b.callback_query;\nconst msg = b.message || b.edited_message;\n\n// Guarda o chat de quem fala com o bot, para o lembrete do dia 8 saber o destino.\nconst guardarChat = (chat) => {\n  if (!chat || !chat.id) return;\n  let cfg = {};\n  try { cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8')); } catch (e) {}\n  if (cfg.chat_id !== chat.id) {\n    cfg.chat_id = chat.id;\n    cfg.nome = [chat.first_name, chat.last_name].filter(Boolean).join(' ') || chat.username || '';\n    cfg.atualizado_em = new Date().toISOString();\n    fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));\n  }\n};\n\nif (cb) {\n  guardarChat(cb.message.chat);\n  const acao = String(cb.data || '').split(':')[0];\n  return [{ json: {\n    tipo: acao === 'ok' ? 'confirmar' : 'cancelar',\n    chat_id: cb.message.chat.id,\n    message_id: cb.message.message_id,\n    callback_query_id: cb.id,\n  }}];\n}\n\nif (msg) guardarChat(msg.chat);\n\nif (msg && msg.document) {\n  const d = msg.document;\n  const ehPdf = d.mime_type === 'application/pdf' || /\\.pdf$/i.test(d.file_name || '');\n  return [{ json: {\n    tipo: ehPdf ? 'documento' : 'ignorar',\n    chat_id: msg.chat.id,\n    file_id: d.file_id,\n    file_name: d.file_name || 'nota.pdf',\n    aviso: ehPdf ? '' : 'Me manda o PDF da nota, esse arquivo nao e PDF.',\n  }}];\n}\n\nreturn [{ json: {\n  tipo: 'ignorar',\n  chat_id: msg && msg.chat ? msg.chat.id : null,\n  aviso: 'Oi! Sou o assistente contábil da Sino.\\n\\nTodo dia 8 eu te lembro da NF da Prefeitura de Oeiras. É só me mandar o PDF da nota aqui que eu monto o e-mail com as certidões e envio depois da sua confirmação.',\n}}];\n"},"id":"rotear","name":"Rotear","type":"n8n-nodes-base.code","typeVersion":2,"position":[-160,300]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"documento","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-documento?","name":"É documento?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,300]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"confirmar","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-confirmação?","name":"É confirmação?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,560]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"cancelar","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-cancelamento?","name":"É cancelamento?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,800]},{"parameters":{"url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/getFile","sendQuery":true,"queryParameters":{"parameters":[{"name":"file_id","value":"={{ $json.file_id }}"}]},"options":{}},"id":"getfile","name":"Baixar do Telegram","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,200]},{"parameters":{"url":"=https://api.telegram.org/file/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/{{ $json.result.file_path }}","options":{"response":{"response":{"responseFormat":"file","outputPropertyName":"data"}}}},"id":"download","name":"Arquivo PDF","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[500,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst { execFileSync } = require('child_process');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\n\nfs.mkdirSync(DIR, { recursive: true });\n\n// Pega o PDF vindo do Telegram, seja em memoria ou no filesystem do n8n.\nlet buf;\ntry {\n  buf = await this.helpers.getBinaryDataBuffer(0, 'data');\n} catch (e) {\n  buf = Buffer.from($input.first().binary.data.data, 'base64');\n}\nconst destino = DIR + '/nota.pdf';\nfs.writeFileSync(destino, buf);\n\n// Competencia esperada = mes anterior ao de hoje, no fuso de Brasilia.\nconst agora = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/Fortaleza' }));\nconst ant = new Date(agora.getFullYear(), agora.getMonth() - 1, 1);\nconst esperada = String(ant.getMonth() + 1).padStart(2, '0') + '/' + ant.getFullYear();\n\nconst saida = execFileSync('python3',\n  ['/home/ultimo/nf-pmo/analisar_nota.py', destino, esperada],\n  { encoding: 'utf8', timeout: 60000 });\n\nconst dados = JSON.parse(saida);\ndados.chat_id = $('Rotear').first().json.chat_id;\ndados.arquivo_nota = destino;\nreturn [{ json: dados }];\n"},"id":"analisar-nota","name":"Analisar nota","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\nconst analise = $('Analisar nota').first().json;\n\nfor (const f of fs.readdirSync(DIR)) {\n  if (f.startsWith('cert_')) fs.unlinkSync(DIR + '/' + f);\n}\n\n// As certidoes vem da pasta certidoes/, que e a fonte da verdade: trocar uma\n// certidao e so substituir o PDF la e ajustar a validade em validade.json.\n// Antes vinham dos anexos do ultimo e-mail enviado, o que repetia para sempre\n// o pacote anterior, inclusive certidao ja vencida.\nconst CERT_DIR = '/home/ultimo/nf-pmo/certidoes';\nconst CATALOGO = CERT_DIR + '/validade.json';\nif (!fs.existsSync(CATALOGO)) {\n  throw new Error('Nao achei ' + CATALOGO + '. Sem ele nao sei quais certidoes anexar.');\n}\nconst CAT = JSON.parse(fs.readFileSync(CATALOGO, 'utf8'));\n\nconst hoje = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Fortaleza' });\nconst emBR = (iso) => iso.slice(8, 10) + '/' + iso.slice(5, 7) + '/' + iso.slice(0, 4);\nconst diasAte = (iso) => Math.round(\n  (Date.parse(iso + 'T12:00:00Z') - Date.parse(hoje + 'T12:00:00Z')) / 86400000);\n\nconst certidoes = [];\nconst linhasCert = [];\nconst alertasCert = [];\n\nfor (const arquivo of Object.keys(CAT)) {\n  const info = CAT[arquivo];\n  const origem = CERT_DIR + '/' + arquivo;\n  if (!fs.existsSync(origem)) {\n    alertasCert.push('Falta ' + info.nome + ' na pasta (' + arquivo + '). NAO foi anexada.');\n    continue;\n  }\n  const caminho = DIR + '/cert_' + certidoes.length + '_' + arquivo.replace(/[^\\w.-]/g, '_');\n  fs.copyFileSync(origem, caminho);\n  certidoes.push({ nome: arquivo, caminho });\n\n  const dias = diasAte(info.validade);\n  let marca = '✅';\n  if (dias < 0) {\n    marca = '⛔';\n    alertasCert.push(info.nome + ' VENCEU em ' + emBR(info.validade) + '. Emita outra antes de enviar.');\n  } else if (dias <= 15) {\n    marca = '⏳';\n    alertasCert.push(info.nome + ' vence em ' + emBR(info.validade) + ', daqui a ' + dias + ' dia(s).');\n  }\n  linhasCert.push(marca + ' ' + info.nome + ' · até ' + emBR(info.validade));\n  if (info.observacao) alertasCert.push(info.nome + ' ' + info.observacao + '.');\n}\n\n// PDF solto na pasta nao entra no e-mail calado: avisa que ficou de fora.\nfor (const f of fs.readdirSync(CERT_DIR)) {\n  if (/\\.pdf$/i.test(f) && !CAT[f]) {\n    alertasCert.push(f + ' está na pasta mas não consta em validade.json, então ficou de fora.');\n  }\n}\n\n// A referencia e o mes do SERVICO (mes anterior), nao a competencia da nota.\nconst ref = analise.referencia;\nconst estado = {\n  referencia: ref,\n  competencia_nota: analise.competencia_nota,\n  mes_extenso: analise.mes_extenso,\n  ano: analise.ano,\n  valor: analise.valor,\n  numero: analise.numero,\n  nota: analise.arquivo_nota,\n  certidoes,\n  assunto: 'PAGAMENTO ' + ref + ' | NF Publicidade PMO',\n  criado_em: new Date().toISOString(),\n};\nfs.writeFileSync(DIR + '/estado.json', JSON.stringify(estado, null, 2));\n\nlet jaEnviado = null;\nconst LOG = '/home/ultimo/nf-pmo/enviados.json';\nif (fs.existsSync(LOG)) {\n  const log = JSON.parse(fs.readFileSync(LOG, 'utf8'));\n  jaEnviado = log.find(e => e.referencia === ref) || null;\n}\n\nconst esc = (t) => String(t)\n  .split('&').join('&amp;')\n  .split('<').join('&lt;')\n  .split('>').join('&gt;');\n\nconst L = [];\nL.push('<b>NF ' + esc(analise.mes_extenso + '/' + analise.ano) + '  ·  PMO</b>');\nL.push('');\nif (analise.numero) L.push('Nota nº ' + esc(analise.numero));\nif (analise.valor) L.push('Valor: ' + esc(analise.valor));\nif (analise.competencia_nota) L.push('Competência na nota: ' + esc(analise.competencia_nota));\nL.push('');\nL.push('<b>Certidões anexadas: ' + certidoes.length + '</b>');\nfor (const c of linhasCert) L.push(esc(c));\nL.push('');\nL.push('<b>Assunto:</b> ' + esc(estado.assunto));\nL.push('<b>Para:</b> protocolo, controladoria e prefeituraoeiras');\nif (analise.avisos && analise.avisos.length) {\n  L.push('');\n  for (const a of analise.avisos) L.push('⚠️ ' + esc(a));\n}\nif (alertasCert.length) {\n  L.push('');\n  for (const a of alertasCert) L.push('⚠️ ' + esc(a));\n}\nif (jaEnviado) {\n  L.push('');\n  L.push('🔁 A referência ' + esc(ref) + ' já foi enviada em ' +\n         new Date(jaEnviado.em).toLocaleDateString('pt-BR') + '.');\n}\nL.push('');\nL.push('Envio?');\n\n// Monta o corpo inteiro aqui. Se deixar objeto aninhado na expressao do n8n,\n// o '}}' interno e lido como fim da expressao e quebra com 'invalid syntax'.\nconst body = JSON.stringify({\n  chat_id: analise.chat_id,\n  text: L.join('\\n'),\n  parse_mode: 'HTML',\n  reply_markup: {\n    inline_keyboard: [[\n      { text: '✅ Enviar', callback_data: 'ok' },\n      { text: '❌ Cancelar', callback_data: 'no' },\n    ]],\n  },\n});\n\nreturn [{ json: {\n  chat_id: analise.chat_id,\n  texto: L.join('\\n'),\n  total_certidoes: certidoes.length,\n  body,\n}}];\n"},"id":"montar-pacote","name":"Montar pacote","type":"n8n-nodes-base.code","typeVersion":2,"position":[940,200]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ $json.body }}","options":{}},"id":"pedir-confirmação","name":"Pedir confirmação","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1160,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\nconst rota = $('Rotear').first().json;\n\nif (!fs.existsSync(DIR + '/estado.json')) {\n  return [{ json: { erro: 'Não há nota pendente. Me manda o PDF de novo.', chat_id: rota.chat_id } }];\n}\nconst estado = JSON.parse(fs.readFileSync(DIR + '/estado.json', 'utf8'));\n\nconst DESTINOS = 'protocolo@oeiras.pi.gov.br, controladoria@oeiras.pi.gov.br, prefeituraoeiras@gmail.com';\n// Autentica como ultimofilgueira, mas assina como ultimocampos, que e o\n// remetente que a prefeitura conhece. O alias precisa estar verificado no Gmail.\nconst REMETENTE = '=?UTF-8?B?' + Buffer.from('Último Campos', 'utf8').toString('base64') +\n                  '?= <ultimocampos@gmail.com>';\n\nconst ehAscii = (txt) => Array.from(txt).every(c => c.charCodeAt(0) < 128);\nconst rfc2047 = (txt) => ehAscii(txt)\n  ? txt\n  : '=?UTF-8?B?' + Buffer.from(txt, 'utf8').toString('base64') + '?=';\n\nconst quebrar = (b64) => {\n  const partes = [];\n  for (let i = 0; i < b64.length; i += 76) partes.push(b64.slice(i, i + 76));\n  return partes.join('\\r\\n');\n};\n\nconst corpo = 'Olá,\\n\\nSegue anexo as certidões atualizadas e Nota Fiscal referente aos serviços ' +\n              'prestados em ' + estado.mes_extenso + ' de ' + estado.ano +\n              ' ao município de Oeiras.\\n--\\nAtt,';\n\nconst nomeNota = 'NF_PMO_' + estado.ano + '_' +\n                 estado.mes_extenso.split('Ç').join('C') + '_COMUNICACAO.pdf';\n\nconst anexos = estado.certidoes.map(c => ({ nome: c.nome, caminho: c.caminho }));\nanexos.push({ nome: nomeNota, caminho: estado.nota });\n\nconst limite = 'sino' + Date.now().toString(36);\nlet mime = '';\nmime += 'From: ' + REMETENTE + '\\r\\n';\nmime += 'To: ' + DESTINOS + '\\r\\n';\nmime += 'Subject: ' + rfc2047(estado.assunto) + '\\r\\n';\nmime += 'MIME-Version: 1.0\\r\\n';\nmime += 'Content-Type: multipart/mixed; boundary=\"' + limite + '\"\\r\\n\\r\\n';\n\nmime += '--' + limite + '\\r\\n';\nmime += 'Content-Type: text/plain; charset=\"UTF-8\"\\r\\n';\nmime += 'Content-Transfer-Encoding: base64\\r\\n\\r\\n';\nmime += quebrar(Buffer.from(corpo, 'utf8').toString('base64')) + '\\r\\n\\r\\n';\n\nlet bytes = 0;\nfor (const a of anexos) {\n  const buf = fs.readFileSync(a.caminho);\n  bytes += buf.length;\n  mime += '--' + limite + '\\r\\n';\n  mime += 'Content-Type: application/pdf; name=\"' + a.nome + '\"\\r\\n';\n  mime += 'Content-Disposition: attachment; filename=\"' + a.nome + '\"\\r\\n';\n  mime += 'Content-Transfer-Encoding: base64\\r\\n\\r\\n';\n  mime += quebrar(buf.toString('base64')) + '\\r\\n\\r\\n';\n}\nmime += '--' + limite + '--\\r\\n';\n\nreturn [{\n  json: {\n    chat_id: rota.chat_id,\n    assunto: estado.assunto,\n    referencia: estado.referencia,\n    total: anexos.length,\n    mb: (bytes / 1048576).toFixed(1),\n  },\n  binary: {\n    mime: {\n      data: Buffer.from(mime, 'utf8').toString('base64'),\n      mimeType: 'message/rfc822',\n      fileName: 'email.eml',\n    },\n  },\n}];\n"},"id":"carregar-pendente","name":"Carregar pendente","type":"n8n-nodes-base.code","typeVersion":2,"position":[280,480]},{"parameters":{"method":"POST","url":"https://gmail.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media","authentication":"predefinedCredentialType","nodeCredentialType":"gmailOAuth2","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"message/rfc822"}]},"sendBody":true,"contentType":"binaryData","inputDataFieldName":"mime","options":{}},"id":"enviar","name":"Enviar e-mail","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[500,480],"credentials":{"gmailOAuth2":{"id":"JrDIhubWWqvyxQA7","name":"Gmail account"}}},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst LOG = '/home/ultimo/nf-pmo/enviados.json';\nconst ARQUIVO = '/home/ultimo/nf-pmo/notas';\nconst dados = $('Carregar pendente').first().json;\nconst estado = JSON.parse(fs.readFileSync('/home/ultimo/nf-pmo/pendente/estado.json', 'utf8'));\n\n// Tipo do servico no nome do arquivo. Mudou de PUBLICIDADE para COMUNICACAO\n// em marco/2026; fica em config-nf.json para nao precisar mexer no workflow.\nlet tipo = 'COMUNICACAO';\ntry {\n  tipo = JSON.parse(fs.readFileSync('/home/ultimo/nf-pmo/config-nf.json', 'utf8')).tipo || tipo;\n} catch (e) {}\n\n// Mesmo padrao da pasta do Mac: NF_PMO_<ANO>_<MES>_<TIPO>.pdf, MARCO sem cedilha\nconst nomeArquivo = 'NF_PMO_' + estado.ano + '_' +\n                    estado.mes_extenso.replace('Ç', 'C') + '_' + tipo + '.pdf';\n\nfs.mkdirSync(ARQUIVO, { recursive: true });\nfs.copyFileSync(estado.nota, ARQUIVO + '/' + nomeArquivo);\n\nconst log = fs.existsSync(LOG) ? JSON.parse(fs.readFileSync(LOG, 'utf8')) : [];\nlog.push({\n  referencia: dados.referencia,\n  assunto: dados.assunto,\n  anexos: dados.total,\n  arquivo: nomeArquivo,\n  em: new Date().toISOString(),\n  gmail_id: $json.id || null,\n});\nfs.writeFileSync(LOG, JSON.stringify(log, null, 2));\n\nreturn [{ json: {\n  chat_id: dados.chat_id,\n  texto: '✅ Enviado.\\n\\n' + dados.assunto + '\\n' + dados.total +\n         ' anexos para protocolo, controladoria e prefeituraoeiras.\\n\\n' +\n         '📁 Arquivado como ' + nomeArquivo + '\\n\\n' +\n         'Se ninguém responder em 48h eu te aviso.',\n}}];\n"},"id":"registrar-envio","name":"Registrar envio","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,480]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: $json.texto}) }}","options":{}},"id":"avisar-enviado","name":"Avisar enviado","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[940,480]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '❌ Cancelado. Nada foi enviado.'}) }}","options":{}},"id":"avisar-cancelado","name":"Avisar cancelado","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,700]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '📄 Recebi a nota. Estou lendo os dados e buscando as certidões...'}) }}","options":{}},"id":"avisar-recebido","name":"Avisar recebido","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,40]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '📤 Enviando para a prefeitura...'}) }}","options":{}},"id":"avisar-enviando","name":"Avisar enviando","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,620]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: $json.aviso}) }}","options":{}},"id":"orientar","name":"Orientar","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,860]}],"connections":{"Telegram (webhook)":{"main":[[{"node":"Rotear","type":"main","index":0}]]},"Rotear":{"main":[[{"node":"É documento?","type":"main","index":0}]]},"É documento?":{"main":[[{"node":"Baixar do Telegram","type":"main","index":0},{"node":"Avisar recebido","type":"main","index":0}],[{"node":"É confirmação?","type":"main","index":0}]]},"Baixar do Telegram":{"main":[[{"node":"Arquivo PDF","type":"main","index":0}]]},"Arquivo PDF":{"main":[[{"node":"Analisar nota","type":"main","index":0}]]},"Analisar nota":{"main":[[{"node":"Montar pacote","type":"main","index":0}]]},"Montar pacote":{"main":[[{"node":"Pedir confirmação","type":"main","index":0}]]},"É confirmação?":{"main":[[{"node":"Carregar pendente","type":"main","index":0},{"node":"Avisar enviando","type":"main","index":0}],[{"node":"É cancelamento?","type":"main","index":0}]]},"É cancelamento?":{"main":[[{"node":"Avisar cancelado","type":"main","index":0}],[{"node":"Orientar","type":"main","index":0}]]},"Carregar pendente":{"main":[[{"node":"Enviar e-mail","type":"main","index":0}]]},"Enviar e-mail":{"main":[[{"node":"Registrar envio","type":"main","index":0}]]},"Registrar envio":{"main":[[{"node":"Avisar enviado","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false,"timezone":"America/Fortaleza","errorWorkflow":"rq9KzWVq6da7YiNQ"},"staticData":null,"meta":null,"pinData":null,"versionId":"6040e360-8a13-457d-9f22-bb22668bc10b","activeVersionId":"6040e360-8a13-457d-9f22-bb22668bc10b","versionCounter":64,"triggerCount":1,"shared":[{"updatedAt":"2026-08-09T15:26:46.479Z","createdAt":"2026-08-09T15:26:46.479Z","role":"workflow:owner","workflowId":"2QNMnPXfarXJB1Km","projectId":"lcNbFUrydUsxxeGp","project":{"updatedAt":"2026-05-06T16:07:04.097Z","createdAt":"2026-04-10T15:23:11.499Z","id":"lcNbFUrydUsxxeGp","name":"Ultimo Campos <ultimocampos@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"}}],"tags":[],"activeVersion":{"updatedAt":"2026-08-17T12:10:27.852Z","createdAt":"2026-08-17T12:10:27.852Z","versionId":"6040e360-8a13-457d-9f22-bb22668bc10b","workflowId":"2QNMnPXfarXJB1Km","nodes":[{"parameters":{"httpMethod":"POST","path":"sino-contabil","responseMode":"onReceived","options":{}},"id":"webhook","name":"Telegram (webhook)","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[-380,300],"webhookId":"sino-contabil-nfpmo"},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst CONFIG = '/home/ultimo/nf-pmo/config.json';\nconst b = $input.first().json.body || {};\nconst cb = b.callback_query;\nconst msg = b.message || b.edited_message;\n\n// Guarda o chat de quem fala com o bot, para o lembrete do dia 8 saber o destino.\nconst guardarChat = (chat) => {\n  if (!chat || !chat.id) return;\n  let cfg = {};\n  try { cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8')); } catch (e) {}\n  if (cfg.chat_id !== chat.id) {\n    cfg.chat_id = chat.id;\n    cfg.nome = [chat.first_name, chat.last_name].filter(Boolean).join(' ') || chat.username || '';\n    cfg.atualizado_em = new Date().toISOString();\n    fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));\n  }\n};\n\nif (cb) {\n  guardarChat(cb.message.chat);\n  const acao = String(cb.data || '').split(':')[0];\n  return [{ json: {\n    tipo: acao === 'ok' ? 'confirmar' : 'cancelar',\n    chat_id: cb.message.chat.id,\n    message_id: cb.message.message_id,\n    callback_query_id: cb.id,\n  }}];\n}\n\nif (msg) guardarChat(msg.chat);\n\nif (msg && msg.document) {\n  const d = msg.document;\n  const ehPdf = d.mime_type === 'application/pdf' || /\\.pdf$/i.test(d.file_name || '');\n  return [{ json: {\n    tipo: ehPdf ? 'documento' : 'ignorar',\n    chat_id: msg.chat.id,\n    file_id: d.file_id,\n    file_name: d.file_name || 'nota.pdf',\n    aviso: ehPdf ? '' : 'Me manda o PDF da nota, esse arquivo nao e PDF.',\n  }}];\n}\n\nreturn [{ json: {\n  tipo: 'ignorar',\n  chat_id: msg && msg.chat ? msg.chat.id : null,\n  aviso: 'Oi! Sou o assistente contábil da Sino.\\n\\nTodo dia 8 eu te lembro da NF da Prefeitura de Oeiras. É só me mandar o PDF da nota aqui que eu monto o e-mail com as certidões e envio depois da sua confirmação.',\n}}];\n"},"id":"rotear","name":"Rotear","type":"n8n-nodes-base.code","typeVersion":2,"position":[-160,300]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"documento","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-documento?","name":"É documento?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,300]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"confirmar","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-confirmação?","name":"É confirmação?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,560]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"loose","version":2},"conditions":[{"id":"c1","leftValue":"={{ $json.tipo }}","rightValue":"cancelar","operator":{"type":"string","operation":"equals"}}],"combinator":"and"},"options":{}},"id":"é-cancelamento?","name":"É cancelamento?","type":"n8n-nodes-base.if","typeVersion":2,"position":[60,800]},{"parameters":{"url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/getFile","sendQuery":true,"queryParameters":{"parameters":[{"name":"file_id","value":"={{ $json.file_id }}"}]},"options":{}},"id":"getfile","name":"Baixar do Telegram","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,200]},{"parameters":{"url":"=https://api.telegram.org/file/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/{{ $json.result.file_path }}","options":{"response":{"response":{"responseFormat":"file","outputPropertyName":"data"}}}},"id":"download","name":"Arquivo PDF","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[500,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst { execFileSync } = require('child_process');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\n\nfs.mkdirSync(DIR, { recursive: true });\n\n// Pega o PDF vindo do Telegram, seja em memoria ou no filesystem do n8n.\nlet buf;\ntry {\n  buf = await this.helpers.getBinaryDataBuffer(0, 'data');\n} catch (e) {\n  buf = Buffer.from($input.first().binary.data.data, 'base64');\n}\nconst destino = DIR + '/nota.pdf';\nfs.writeFileSync(destino, buf);\n\n// Competencia esperada = mes anterior ao de hoje, no fuso de Brasilia.\nconst agora = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/Fortaleza' }));\nconst ant = new Date(agora.getFullYear(), agora.getMonth() - 1, 1);\nconst esperada = String(ant.getMonth() + 1).padStart(2, '0') + '/' + ant.getFullYear();\n\nconst saida = execFileSync('python3',\n  ['/home/ultimo/nf-pmo/analisar_nota.py', destino, esperada],\n  { encoding: 'utf8', timeout: 60000 });\n\nconst dados = JSON.parse(saida);\ndados.chat_id = $('Rotear').first().json.chat_id;\ndados.arquivo_nota = destino;\nreturn [{ json: dados }];\n"},"id":"analisar-nota","name":"Analisar nota","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\nconst analise = $('Analisar nota').first().json;\n\nfor (const f of fs.readdirSync(DIR)) {\n  if (f.startsWith('cert_')) fs.unlinkSync(DIR + '/' + f);\n}\n\n// As certidoes vem da pasta certidoes/, que e a fonte da verdade: trocar uma\n// certidao e so substituir o PDF la e ajustar a validade em validade.json.\n// Antes vinham dos anexos do ultimo e-mail enviado, o que repetia para sempre\n// o pacote anterior, inclusive certidao ja vencida.\nconst CERT_DIR = '/home/ultimo/nf-pmo/certidoes';\nconst CATALOGO = CERT_DIR + '/validade.json';\nif (!fs.existsSync(CATALOGO)) {\n  throw new Error('Nao achei ' + CATALOGO + '. Sem ele nao sei quais certidoes anexar.');\n}\nconst CAT = JSON.parse(fs.readFileSync(CATALOGO, 'utf8'));\n\nconst hoje = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Fortaleza' });\nconst emBR = (iso) => iso.slice(8, 10) + '/' + iso.slice(5, 7) + '/' + iso.slice(0, 4);\nconst diasAte = (iso) => Math.round(\n  (Date.parse(iso + 'T12:00:00Z') - Date.parse(hoje + 'T12:00:00Z')) / 86400000);\n\nconst certidoes = [];\nconst linhasCert = [];\nconst alertasCert = [];\n\nfor (const arquivo of Object.keys(CAT)) {\n  const info = CAT[arquivo];\n  const origem = CERT_DIR + '/' + arquivo;\n  if (!fs.existsSync(origem)) {\n    alertasCert.push('Falta ' + info.nome + ' na pasta (' + arquivo + '). NAO foi anexada.');\n    continue;\n  }\n  const caminho = DIR + '/cert_' + certidoes.length + '_' + arquivo.replace(/[^\\w.-]/g, '_');\n  fs.copyFileSync(origem, caminho);\n  certidoes.push({ nome: arquivo, caminho });\n\n  const dias = diasAte(info.validade);\n  let marca = '✅';\n  if (dias < 0) {\n    marca = '⛔';\n    alertasCert.push(info.nome + ' VENCEU em ' + emBR(info.validade) + '. Emita outra antes de enviar.');\n  } else if (dias <= 15) {\n    marca = '⏳';\n    alertasCert.push(info.nome + ' vence em ' + emBR(info.validade) + ', daqui a ' + dias + ' dia(s).');\n  }\n  linhasCert.push(marca + ' ' + info.nome + ' · até ' + emBR(info.validade));\n  if (info.observacao) alertasCert.push(info.nome + ' ' + info.observacao + '.');\n}\n\n// PDF solto na pasta nao entra no e-mail calado: avisa que ficou de fora.\nfor (const f of fs.readdirSync(CERT_DIR)) {\n  if (/\\.pdf$/i.test(f) && !CAT[f]) {\n    alertasCert.push(f + ' está na pasta mas não consta em validade.json, então ficou de fora.');\n  }\n}\n\n// A referencia e o mes do SERVICO (mes anterior), nao a competencia da nota.\nconst ref = analise.referencia;\nconst estado = {\n  referencia: ref,\n  competencia_nota: analise.competencia_nota,\n  mes_extenso: analise.mes_extenso,\n  ano: analise.ano,\n  valor: analise.valor,\n  numero: analise.numero,\n  nota: analise.arquivo_nota,\n  certidoes,\n  assunto: 'PAGAMENTO ' + ref + ' | NF Publicidade PMO',\n  criado_em: new Date().toISOString(),\n};\nfs.writeFileSync(DIR + '/estado.json', JSON.stringify(estado, null, 2));\n\nlet jaEnviado = null;\nconst LOG = '/home/ultimo/nf-pmo/enviados.json';\nif (fs.existsSync(LOG)) {\n  const log = JSON.parse(fs.readFileSync(LOG, 'utf8'));\n  jaEnviado = log.find(e => e.referencia === ref) || null;\n}\n\nconst esc = (t) => String(t)\n  .split('&').join('&amp;')\n  .split('<').join('&lt;')\n  .split('>').join('&gt;');\n\nconst L = [];\nL.push('<b>NF ' + esc(analise.mes_extenso + '/' + analise.ano) + '  ·  PMO</b>');\nL.push('');\nif (analise.numero) L.push('Nota nº ' + esc(analise.numero));\nif (analise.valor) L.push('Valor: ' + esc(analise.valor));\nif (analise.competencia_nota) L.push('Competência na nota: ' + esc(analise.competencia_nota));\nL.push('');\nL.push('<b>Certidões anexadas: ' + certidoes.length + '</b>');\nfor (const c of linhasCert) L.push(esc(c));\nL.push('');\nL.push('<b>Assunto:</b> ' + esc(estado.assunto));\nL.push('<b>Para:</b> protocolo, controladoria e prefeituraoeiras');\nif (analise.avisos && analise.avisos.length) {\n  L.push('');\n  for (const a of analise.avisos) L.push('⚠️ ' + esc(a));\n}\nif (alertasCert.length) {\n  L.push('');\n  for (const a of alertasCert) L.push('⚠️ ' + esc(a));\n}\nif (jaEnviado) {\n  L.push('');\n  L.push('🔁 A referência ' + esc(ref) + ' já foi enviada em ' +\n         new Date(jaEnviado.em).toLocaleDateString('pt-BR') + '.');\n}\nL.push('');\nL.push('Envio?');\n\n// Monta o corpo inteiro aqui. Se deixar objeto aninhado na expressao do n8n,\n// o '}}' interno e lido como fim da expressao e quebra com 'invalid syntax'.\nconst body = JSON.stringify({\n  chat_id: analise.chat_id,\n  text: L.join('\\n'),\n  parse_mode: 'HTML',\n  reply_markup: {\n    inline_keyboard: [[\n      { text: '✅ Enviar', callback_data: 'ok' },\n      { text: '❌ Cancelar', callback_data: 'no' },\n    ]],\n  },\n});\n\nreturn [{ json: {\n  chat_id: analise.chat_id,\n  texto: L.join('\\n'),\n  total_certidoes: certidoes.length,\n  body,\n}}];\n"},"id":"montar-pacote","name":"Montar pacote","type":"n8n-nodes-base.code","typeVersion":2,"position":[940,200]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ $json.body }}","options":{}},"id":"pedir-confirmação","name":"Pedir confirmação","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1160,200]},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst DIR = '/home/ultimo/nf-pmo/pendente';\nconst rota = $('Rotear').first().json;\n\nif (!fs.existsSync(DIR + '/estado.json')) {\n  return [{ json: { erro: 'Não há nota pendente. Me manda o PDF de novo.', chat_id: rota.chat_id } }];\n}\nconst estado = JSON.parse(fs.readFileSync(DIR + '/estado.json', 'utf8'));\n\nconst DESTINOS = 'protocolo@oeiras.pi.gov.br, controladoria@oeiras.pi.gov.br, prefeituraoeiras@gmail.com';\n// Autentica como ultimofilgueira, mas assina como ultimocampos, que e o\n// remetente que a prefeitura conhece. O alias precisa estar verificado no Gmail.\nconst REMETENTE = '=?UTF-8?B?' + Buffer.from('Último Campos', 'utf8').toString('base64') +\n                  '?= <ultimocampos@gmail.com>';\n\nconst ehAscii = (txt) => Array.from(txt).every(c => c.charCodeAt(0) < 128);\nconst rfc2047 = (txt) => ehAscii(txt)\n  ? txt\n  : '=?UTF-8?B?' + Buffer.from(txt, 'utf8').toString('base64') + '?=';\n\nconst quebrar = (b64) => {\n  const partes = [];\n  for (let i = 0; i < b64.length; i += 76) partes.push(b64.slice(i, i + 76));\n  return partes.join('\\r\\n');\n};\n\nconst corpo = 'Olá,\\n\\nSegue anexo as certidões atualizadas e Nota Fiscal referente aos serviços ' +\n              'prestados em ' + estado.mes_extenso + ' de ' + estado.ano +\n              ' ao município de Oeiras.\\n--\\nAtt,';\n\nconst nomeNota = 'NF_PMO_' + estado.ano + '_' +\n                 estado.mes_extenso.split('Ç').join('C') + '_COMUNICACAO.pdf';\n\nconst anexos = estado.certidoes.map(c => ({ nome: c.nome, caminho: c.caminho }));\nanexos.push({ nome: nomeNota, caminho: estado.nota });\n\nconst limite = 'sino' + Date.now().toString(36);\nlet mime = '';\nmime += 'From: ' + REMETENTE + '\\r\\n';\nmime += 'To: ' + DESTINOS + '\\r\\n';\nmime += 'Subject: ' + rfc2047(estado.assunto) + '\\r\\n';\nmime += 'MIME-Version: 1.0\\r\\n';\nmime += 'Content-Type: multipart/mixed; boundary=\"' + limite + '\"\\r\\n\\r\\n';\n\nmime += '--' + limite + '\\r\\n';\nmime += 'Content-Type: text/plain; charset=\"UTF-8\"\\r\\n';\nmime += 'Content-Transfer-Encoding: base64\\r\\n\\r\\n';\nmime += quebrar(Buffer.from(corpo, 'utf8').toString('base64')) + '\\r\\n\\r\\n';\n\nlet bytes = 0;\nfor (const a of anexos) {\n  const buf = fs.readFileSync(a.caminho);\n  bytes += buf.length;\n  mime += '--' + limite + '\\r\\n';\n  mime += 'Content-Type: application/pdf; name=\"' + a.nome + '\"\\r\\n';\n  mime += 'Content-Disposition: attachment; filename=\"' + a.nome + '\"\\r\\n';\n  mime += 'Content-Transfer-Encoding: base64\\r\\n\\r\\n';\n  mime += quebrar(buf.toString('base64')) + '\\r\\n\\r\\n';\n}\nmime += '--' + limite + '--\\r\\n';\n\nreturn [{\n  json: {\n    chat_id: rota.chat_id,\n    assunto: estado.assunto,\n    referencia: estado.referencia,\n    total: anexos.length,\n    mb: (bytes / 1048576).toFixed(1),\n  },\n  binary: {\n    mime: {\n      data: Buffer.from(mime, 'utf8').toString('base64'),\n      mimeType: 'message/rfc822',\n      fileName: 'email.eml',\n    },\n  },\n}];\n"},"id":"carregar-pendente","name":"Carregar pendente","type":"n8n-nodes-base.code","typeVersion":2,"position":[280,480]},{"parameters":{"method":"POST","url":"https://gmail.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media","authentication":"predefinedCredentialType","nodeCredentialType":"gmailOAuth2","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"message/rfc822"}]},"sendBody":true,"contentType":"binaryData","inputDataFieldName":"mime","options":{}},"id":"enviar","name":"Enviar e-mail","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[500,480],"credentials":{"gmailOAuth2":{"id":"JrDIhubWWqvyxQA7","name":"Gmail account"}}},{"parameters":{"jsCode":"\nconst fs = require('fs');\nconst LOG = '/home/ultimo/nf-pmo/enviados.json';\nconst ARQUIVO = '/home/ultimo/nf-pmo/notas';\nconst dados = $('Carregar pendente').first().json;\nconst estado = JSON.parse(fs.readFileSync('/home/ultimo/nf-pmo/pendente/estado.json', 'utf8'));\n\n// Tipo do servico no nome do arquivo. Mudou de PUBLICIDADE para COMUNICACAO\n// em marco/2026; fica em config-nf.json para nao precisar mexer no workflow.\nlet tipo = 'COMUNICACAO';\ntry {\n  tipo = JSON.parse(fs.readFileSync('/home/ultimo/nf-pmo/config-nf.json', 'utf8')).tipo || tipo;\n} catch (e) {}\n\n// Mesmo padrao da pasta do Mac: NF_PMO_<ANO>_<MES>_<TIPO>.pdf, MARCO sem cedilha\nconst nomeArquivo = 'NF_PMO_' + estado.ano + '_' +\n                    estado.mes_extenso.replace('Ç', 'C') + '_' + tipo + '.pdf';\n\nfs.mkdirSync(ARQUIVO, { recursive: true });\nfs.copyFileSync(estado.nota, ARQUIVO + '/' + nomeArquivo);\n\nconst log = fs.existsSync(LOG) ? JSON.parse(fs.readFileSync(LOG, 'utf8')) : [];\nlog.push({\n  referencia: dados.referencia,\n  assunto: dados.assunto,\n  anexos: dados.total,\n  arquivo: nomeArquivo,\n  em: new Date().toISOString(),\n  gmail_id: $json.id || null,\n});\nfs.writeFileSync(LOG, JSON.stringify(log, null, 2));\n\nreturn [{ json: {\n  chat_id: dados.chat_id,\n  texto: '✅ Enviado.\\n\\n' + dados.assunto + '\\n' + dados.total +\n         ' anexos para protocolo, controladoria e prefeituraoeiras.\\n\\n' +\n         '📁 Arquivado como ' + nomeArquivo + '\\n\\n' +\n         'Se ninguém responder em 48h eu te aviso.',\n}}];\n"},"id":"registrar-envio","name":"Registrar envio","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,480]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: $json.texto}) }}","options":{}},"id":"avisar-enviado","name":"Avisar enviado","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[940,480]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '❌ Cancelado. Nada foi enviado.'}) }}","options":{}},"id":"avisar-cancelado","name":"Avisar cancelado","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,700]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '📄 Recebi a nota. Estou lendo os dados e buscando as certidões...'}) }}","options":{}},"id":"avisar-recebido","name":"Avisar recebido","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,40]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: '📤 Enviando para a prefeitura...'}) }}","options":{}},"id":"avisar-enviando","name":"Avisar enviando","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,620]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8722984812:AAHOvrtnsBokyTcaJi1ZNmIz8_mBq-ocd-0/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $json.chat_id, text: $json.aviso}) }}","options":{}},"id":"orientar","name":"Orientar","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[280,860]}],"connections":{"Telegram (webhook)":{"main":[[{"node":"Rotear","type":"main","index":0}]]},"Rotear":{"main":[[{"node":"É documento?","type":"main","index":0}]]},"É documento?":{"main":[[{"node":"Baixar do Telegram","type":"main","index":0},{"node":"Avisar recebido","type":"main","index":0}],[{"node":"É confirmação?","type":"main","index":0}]]},"Baixar do Telegram":{"main":[[{"node":"Arquivo PDF","type":"main","index":0}]]},"Arquivo PDF":{"main":[[{"node":"Analisar nota","type":"main","index":0}]]},"Analisar nota":{"main":[[{"node":"Montar pacote","type":"main","index":0}]]},"Montar pacote":{"main":[[{"node":"Pedir confirmação","type":"main","index":0}]]},"É confirmação?":{"main":[[{"node":"Carregar pendente","type":"main","index":0},{"node":"Avisar enviando","type":"main","index":0}],[{"node":"É cancelamento?","type":"main","index":0}]]},"É cancelamento?":{"main":[[{"node":"Avisar cancelado","type":"main","index":0}],[{"node":"Orientar","type":"main","index":0}]]},"Carregar pendente":{"main":[[{"node":"Enviar e-mail","type":"main","index":0}]]},"Enviar e-mail":{"main":[[{"node":"Registrar envio","type":"main","index":0}]]},"Registrar envio":{"main":[[{"node":"Avisar enviado","type":"main","index":0}]]}},"authors":"Ultimo Campos","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-08-17T12:10:27.993Z","id":597,"workflowId":"2QNMnPXfarXJB1Km","versionId":"6040e360-8a13-457d-9f22-bb22668bc10b","event":"activated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-08-17T12:10:27.976Z","id":596,"workflowId":"2QNMnPXfarXJB1Km","versionId":"6040e360-8a13-457d-9f22-bb22668bc10b","event":"deactivated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"}]}}