Add workflow: Supabase → Sheet VENDAS (incremental sync)
This commit is contained in:
@@ -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`
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user