Add workflow: Supabase → Sheet VENDAS (incremental sync)

This commit is contained in:
Hermes Agent
2026-07-10 14:05:44 +00:00
parent 9d2277ba8c
commit ccca4d9db3
2 changed files with 237 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# Supabase → Sheet VENDAS
## O que faz
Busca `fechamentos_web` do Supabase e faz **append** na sheet VENDAS, incremental por `atualizado_em` UTC.
## Pré-requisitos
1. **Supabase account 2 (self-hosted)** — credencial n8n com service role JWT
2. **Google Sheets account** — OAuth com acesso ao spreadsheet `1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk`
3. **Sheet "Sincronizacao"** no spreadsheet — célula A1 com o UTC timestamp do último sync (para o primeiro run, deixar vazio ou usar `1970-01-01T00:00:00Z`)
## Mapa de colunas
| Sheet Col | Supabase |
|-----------|----------|
| DATA | `data` |
| EMPRESA | `loja``Aliança`/`União` |
| SANGRIA | `total_sangrias` |
| DINHEIRO | `fechamento_dinheiro` |
| PIX_TAXA | `total_pixcnpj` |
| PIX_LOJA | `saldo_pixcnpj` |
| PRAZO | `total_areceber` |
| CREDITO | `total_cartao_credito` |
| DEBITO | `total_cartao_debito` |
| VOUCHER | `total_cartao_alimentacao` |
| CARTAO | `fechamento_cartoes` |
| VALES | `total_vales` |
| DESPESAS | `total_despesas` |
| CANCELADOS | `total_cancelamentos` |
| DIFERENCA | `diferenca` |
| CAIXA | `operador` |
| PG | `status_diferenca === 'ok'` |
| T_VENDA | `0` (fórmula na Sheet) |
| TX_CAIXA | `0` (fórmula na Sheet) |
| SISTEMA | `0` (fórmula na Sheet) |
| CLIENTES | `0` (não existe em fechamentos_web) |
## Como instalar
1. Exportar workflow: `Workflows → Add workflow → ⋮ → Import from Code`
2. Copiar o conteúdo de `workflow.js`
3. Configurar credenciais:
- Node `Fetch from Supabase``Supabase account 2 (self-hosted)`
- Nodes `Read Last Sync` / `Append to VENDAS` / `Update Last Sync``Google Sheets account`
4. Criar aba `Sincronizacao` no spreadsheet com célula A1 vazia (primeiro sync vai buscar tudo)
5. Activar workflow
## Notas
- **Schedule**: `0 20 * * *` (20h BRT = 23h UTC)
- O nó `Filter + Map` detecta se há linhas novas; se não houver, faz update do sync marker sem append
- Se o Kong do Supabase mudar de IP (erro 502), corrigir em `/opt/supabase/docker/volumes/api/kong.yml`
+187
View File
@@ -0,0 +1,187 @@
import { workflow, trigger, node } from 'n8n-workflow-sdk';
export default workflow('Supabase → Sheet VENDAS', {
description: 'Busca fechamentos_web do Supabase e append na Sheet VENDAS (incremental por atualizado_em UTC)',
nodes: [
// ── TRIGGER: 20h BRT (23h UTC) ──────────────────────────────────
trigger('Schedule Daily 20h', {
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [100, 300],
parameters: {
rule: { interval: [{ field: 'cron', expression: '0 20 * * *' }] }
}
}),
// ── READ LAST SYNC from Sheet Sincronizacao A1 ───────────────────
node('Read Last Sync', {
type: 'n8n-nodes-base.googleSheets',
typeVersion: 4.2,
position: [340, 300],
credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } },
parameters: {
operation: 'read',
documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' },
sheetName: { __rl: true, mode: 'list', value: 'Sincronizacao' },
cellSelection: { __rl: true, mode: 'list', value: 'A1' },
options: { cellFormat: 'USER_ENTERED' }
}
}),
// ── FETCH fechamentos_web from Supabase ──────────────────────────
node('Fetch from Supabase', {
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [600, 300],
credentials: { supabaseApi: { id: 'Tw9r35a9Iz3P9q7x', name: 'Supabase account 2 (self-hosted)' } },
parameters: {
method: 'GET',
url: '=https://supabase.ofrangao.com.br/rest/v1/fechamentos_web',
queryParameters: {
parameters: [
{
name: 'select',
value: '=id_fechamento,data,loja,turno,operador,fechamento_dinheiro,fechamento_cartoes,total_sangrias,total_despesas,total_vales,total_pixcnpj,saldo_pixcnpj,total_areceber,total_cartao_credito,total_cartao_debito,total_cartao_alimentacao,total_cancelamentos,diferenca,status_diferenca,atualizado_em'
},
{ name: 'order', value: '=atualizado_em.asc' },
{ name: 'limit', value: '=500' }
]
}
}
}),
// ── CODE: Filter by lastSync + Map to Sheet columns ─────────────
node('Filter + Map', {
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [860, 300],
parameters: {
jsCode: `
// 1. lastSync da Sheet
const lastSyncRaw = $input.first().json.value || null;
const lastSync = lastSyncRaw ? new Date(lastSyncRaw).toISOString() : null;
// 2. Todas as rows do Supabase
const allRows = $input.all().map(item => item.json);
// 3. Filtrar por lastSync
let filtered = lastSync ? allRows.filter(r => r.atualizado_em && r.atualizado_em > lastSync) : allRows;
if (filtered.length === 0) {
// Sem linhas novas —marker o sync point com lastSync antigo para não re-buscar
return [{
json: {
_action: 'noop',
_lastSync: lastSync,
_max_updated: lastSync,
DATA:'', EMPRESA:'', SANGRIA:0, DESPESAS:0, VALES:0,
DINHEIRO:0, PIX_TAXA:0, PIX_LOJA:0, PRAZO:0, P2:0,
CREDITO:0, DEBITO:0, VOUCHER:0, CARTAO:0,
T_VENDA:0, TX_CAIXA:0, CLIENTES:0, CANCELADOS:0,
SISTEMA:0, DIFERENCA:0, CAIXA:'', PG:false
}
}];
}
// 4. MAX(atualizado_em) do batch
const maxUpdated = filtered.reduce((m, r) =>
r.atualizado_em > m ? r.atualizado_em : m, filtered[0].atualizado_em);
// 5. Mapear para colunas Sheet
const LOJA_MAP = { alianca: 'Aliança', uniao: 'União' };
return filtered.map(r => ({
json: {
DATA: r.data,
EMPRESA: LOJA_MAP[r.loja] || r.loja,
SANGRIA: r.total_sangrias || 0,
DESPESAS: r.total_despesas || 0,
VALES: r.total_vales || 0,
DINHEIRO: r.fechamento_dinheiro || 0,
PIX_TAXA: r.total_pixcnpj || 0,
PIX_LOJA: r.saldo_pixcnpj || 0,
PRAZO: r.total_areceber || 0,
P2: 0,
CREDITO: r.total_cartao_credito || 0,
DEBITO: r.total_cartao_debito || 0,
VOUCHER: r.total_cartao_alimentacao || 0,
CARTAO: r.fechamento_cartoes || 0,
T_VENDA: 0, // fórmula na Sheet
TX_CAIXA: 0, // fórmula na Sheet
CLIENTES: 0,
CANCELADOS: r.total_cancelamentos || 0,
SISTEMA: 0, // fórmula na Sheet
DIFERENCA: r.diferenca || 0,
CAIXA: r.operador || '',
PG: r.status_diferenca === 'ok',
_action: 'append',
_max_updated: maxUpdated
}
}));
`
}
}),
// ── APPEND to Sheet VENDAS ──────────────────────────────────────
node('Append to VENDAS', {
type: 'n8n-nodes-base.googleSheets',
typeVersion: 4.2,
position: [1100, 200],
credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } },
parameters: {
operation: 'append',
documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' },
sheetName: { __rl: true, mode: 'list', value: 'VENDAS' },
options: { cellFormat: 'USER_ENTERED' }
}
}),
// ── UPDATE LAST SYNC in Sincronizacao A1 ───────────────────────
// Recebe o primeiro item (com _max_updated) de Filter+Map
node('Update Last Sync', {
type: 'n8n-nodes-base.googleSheets',
typeVersion: 4.2,
position: [1100, 440],
credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } },
parameters: {
operation: 'update',
documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' },
sheetName: { __rl: true, mode: 'list', value: 'Sincronizacao' },
cellSelection: { __rl: true, mode: 'list', value: 'A1' },
valueInputOption: 'USER_ENTERED',
options: { cellFormat: 'USER_ENTERED' }
}
}),
],
connections: {
'Schedule Daily 20h': {
main: [[{ node: 'Read Last Sync', type: 'main', index: 0 }]]
},
'Read Last Sync': {
main: [[{ node: 'Fetch from Supabase', type: 'main', index: 0 }]]
},
'Fetch from Supabase': {
main: [[{ node: 'Filter + Map', type: 'main', index: 0 }]]
},
'Filter + Map': {
main: [
// Branch 0: dados para Sheet
[{ node: 'Append to VENDAS', type: 'main', index: 0 }],
// Branch 1: marker sync (vai directo para Update Last Sync)
[{ node: 'Update Last Sync', type: 'main', index: 0 }]
]
},
'Append to VENDAS': {
main: [[{ node: 'Update Last Sync', type: 'main', index: 0 }]]
}
},
settings: {
executionOrder: 'v1',
saveExecutionProgress: true,
saveDataSuccessExecution: 'all',
saveDataErrorExecution: 'all'
}
});