From e56d72c6442ae6c1568d7edcf7d8347d0325fd06 Mon Sep 17 00:00:00 2001 From: Jetour BI Date: Thu, 18 Jun 2026 20:52:36 +0000 Subject: [PATCH] Add n8n service to docker-compose - docker-compose: n8n container (port 5678) with PostgreSQL backend - n8n/: 7 TypeScript workflow files + import script + README - Workflows: 4 CMS BI data ingestion + 3 email reports - README: updated with n8n env vars and port - .dockerignore: exclude import.sh from container --- .dockerignore | 1 + README.md | 8 ++-- docker-compose.yml | 31 +++++++++++++ n8n/README.md | 37 ++++++++++++++++ n8n/import.sh | 34 ++++++++++++++ n8n/wf1_prospects_created.ts | 71 +++++++++++++++++++++++++++++ n8n/wf2_prospects_updated.ts | 71 +++++++++++++++++++++++++++++ n8n/wf3_otps_created.ts | 86 ++++++++++++++++++++++++++++++++++++ n8n/wf4_otps_updated.ts | 86 ++++++++++++++++++++++++++++++++++++ n8n/wf_email_daily.ts | 14 ++++++ n8n/wf_email_monthly.ts | 14 ++++++ n8n/wf_email_weekly.ts | 14 ++++++ 12 files changed, 464 insertions(+), 3 deletions(-) create mode 100644 n8n/README.md create mode 100755 n8n/import.sh create mode 100644 n8n/wf1_prospects_created.ts create mode 100644 n8n/wf2_prospects_updated.ts create mode 100644 n8n/wf3_otps_created.ts create mode 100644 n8n/wf4_otps_updated.ts create mode 100644 n8n/wf_email_daily.ts create mode 100644 n8n/wf_email_monthly.ts create mode 100644 n8n/wf_email_weekly.ts diff --git a/.dockerignore b/.dockerignore index 669d81d..85a3f28 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ __pycache__/ *.log .dbpass migrations/_migrations* +n8n/import.sh diff --git a/README.md b/README.md index 8adf65c..f9c9b85 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ Dashboard handles all SMTP send + recipient resolution. ## Docker Deployment ```bash -# Build and start (app + PostgreSQL) +# Build and start (app + PostgreSQL + n8n) docker compose up -d -# Set a custom DB password -DB_PASSWORD=mysecretpassword docker compose up -d +# Set passwords +DB_PASSWORD=*** N8N_ENCRYPTION_KEY=*** docker compose up -d # View logs docker compose logs -f app @@ -111,10 +111,12 @@ docker compose down -v | `SMTP_USER` | (empty) | SMTP username | | `SMTP_PASS` | (empty) | SMTP password | | `SMTP_FROM` | `reports@jetour.mu` | From address | +| `N8N_ENCRYPTION_KEY` | `change-me-to-a-random-string` | n8n encryption key | ### Ports | Service | Port | Description | |---------|------|-------------| | Dashboard | `8765` | Web UI | +| n8n | `5678` | Workflow automation | | PostgreSQL | `5433` | Mapped from container `5432` | diff --git a/docker-compose.yml b/docker-compose.yml index cca5be3..dea4b81 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,5 +41,36 @@ services: retries: 5 restart: unless-stopped + n8n: + image: n8nio/n8n:latest + container_name: jetour-n8n + ports: + - "5678:5678" + environment: + N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY:-change-me-to-a-random-string}" + DB_TYPE: postgresdb + DB_POSTGRESDB_HOST: db + DB_POSTGRESDB_PORT: "5432" + DB_POSTGRESDB_DATABASE: jetour-bi + DB_POSTGRESDB_USER: jetour + DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD:-jetour} + N8N_HOST: localhost + N8N_PORT: "5678" + N8N_PROTOCOL: http + WEBHOOK_URL: http://localhost:5678/ + volumes: + - n8n_data:/home/node/.n8n + - ./n8n:/workflows + depends_on: + db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + volumes: pgdata: + n8n_data: diff --git a/n8n/README.md b/n8n/README.md new file mode 100644 index 0000000..9df0eb2 --- /dev/null +++ b/n8n/README.md @@ -0,0 +1,37 @@ +# n8n Workflows + +Included in the Docker deployment. Workflows mount at `/workflows/`. + +## After first boot + +1. Open http://localhost:5678 and create your owner account +2. Go to **Settings → n8n API** → Generate an API key +3. Create these credentials in n8n: + +| Credential Name | Type | Config | +|-----------------|------|--------| +| `jetour-bi-remote` | PostgreSQL | Host: `db`, Port: `5432`, DB: `jetour-bi`, User: `jetour`, Password: (your DB_PASSWORD) | +| `jetour-smtp` | SMTP | Your email server details | + +4. Import workflows: +```bash +docker compose exec n8n sh /workflows/import.sh +``` + +## Workflows + +| File | Trigger | What it does | +|------|---------|-------------| +| `wf1_prospects_created.ts` | 02:00 SAST | Fetch new prospects from CMS BI API | +| `wf2_prospects_updated.ts` | 02:10 SAST | Fetch prospect changes from CMS BI | +| `wf3_otps_created.ts` | 02:20 SAST | Fetch new OTPs from CMS BI | +| `wf4_otps_updated.ts` | 02:50 SAST | Fetch OTP changes from CMS BI | +| `wf_email_daily.ts` | 07:00 SAST | POST /api/send-report/daily → email | +| `wf_email_weekly.ts` | Mon 07:00 | POST /api/send-report/weekly → email | +| `wf_email_monthly.ts` | 1st 07:00 | POST /api/send-report/monthly → email | + +## Manual import (alternative) + +If the script fails, use n8n's built-in import in the UI: +1. Go to **Workflows → Import from File** +2. Select each `.ts` file in this directory diff --git a/n8n/import.sh b/n8n/import.sh new file mode 100755 index 0000000..24ae154 --- /dev/null +++ b/n8n/import.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# n8n Workflow Import — run after n8n starts +# Usage: docker compose exec n8n /workflows/import.sh +set -e + +N8N_URL="http://localhost:5678" +WORKFLOWS_DIR="/workflows" + +echo "Waiting for n8n..." +for i in $(seq 1 20); do + if curl -s -o /dev/null "$N8N_URL/healthz" 2>/dev/null; then + echo "n8n ready" + break + fi + sleep 3 +done + +echo "" +echo "To import workflows, you need an n8n API key." +echo "1. Open http://localhost:5678 and create your owner account" +echo "2. Go to Settings → n8n API → Generate API Key" +echo "3. Run: N8N_API_KEY=your-key $0" +echo "" +echo "Workflow files available in $WORKFLOWS_DIR:" +ls -1 "$WORKFLOWS_DIR"/*.ts 2>/dev/null | while read f; do + echo " $(basename $f)" +done +echo "" +echo "Import command (after getting API key):" +echo "" +for wf in "$WORKFLOWS_DIR"/*.ts; do + name=$(basename "$wf" .ts) + echo " n8n import:workflow --input=$wf # $name" +done diff --git a/n8n/wf1_prospects_created.ts b/n8n/wf1_prospects_created.ts new file mode 100644 index 0000000..1d730bb --- /dev/null +++ b/n8n/wf1_prospects_created.ts @@ -0,0 +1,71 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Daily 02:00', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 2 * * *' }] } } } }); + +const auth = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Auth CMS BI', parameters: { method: 'POST', url: 'https://aws-prod-auth.cms360.cloud/api/Jwt/FromBody', + sendBody: true, bodyParameters: { parameters: [{ name: 'username', value: 'JetourMauritiusBI' }, { name: 'password', value: 'j7PW0ev66mJ2' }] }, options: { timeout: 30000 } } } }); + +const setVars = node({ type: 'n8n-nodes-base.set', version: 3.4, + config: { name: 'Set Dates + Token', parameters: { mode: 'manual', includeOtherFields: false, + assignments: { assignments: [ + { id: 't', name: 'token', value: expr('={{ $json.token }}'), type: 'string' }, + { id: 'f', name: 'from', value: expr('={{ DateTime.now().minus({ days: 1 }).startOf("day").toISO() }}'), type: 'string' }, + { id: 't2', name: 'to', value: expr('={{ DateTime.now().startOf("day").toISO() }}'), type: 'string' } + ] } } } }); + +const apiCall = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Fetch Prospects', parameters: { method: 'POST', + url: 'https://bi-api.cmscloud.co.za/api/BusinessIntelligence/Prospects/GetProspectsByDateCreated', + sendBody: true, sendHeaders: true, specifyHeaders: 'keypair', + headerParameters: { parameters: [ + { name: 'Authorization', value: expr('=Bearer {{ $("Set Dates + Token").item.json.token }}') }, + { name: 'Content-Type', value: 'application/json' }] }, + bodyParameters: { parameters: [ + { id: 'f', name: 'from', value: expr('={{ $("Set Dates + Token").item.json.from }}') }, + { id: 't2', name: 'to', value: expr('={{ $("Set Dates + Token").item.json.to }}') }, + { name: 'pageNumber', value: '1' }, { name: 'pageSize', value: '500' }] }, + options: { timeout: 120000 } } } }); + +const flatten = node({ type: 'n8n-nodes-base.code', version: 2, + config: { name: 'Flatten', parameters: { mode: 'runOnceForAllItems', jsCode: +`const items = $input.all(); +const ts = new Date().toISOString().replace(/[-:T]/g,'').slice(0,15); +const bid = "pr_cr_" + ts; +const out = []; +function n(v) { return v || null; } +for (const it of items) { + const d = it.json; + out.push({ json: { _t:'P', pid:d.prospectID, cid:d.contactID, cdr:n(d.cmsDealerRef), kci:n(d.keyloopCustID), + kca:n(d.keyloopCustAccNumber), dd:n(d.dealerDesc), cn:n(d.companyName), ti:n(d.title), + nm:n(d.name), sn:n(d.surname), db:n(d.dtBirth), lg:n(d.language), cl:n(d.cellNumber), + em:n(d.emailAddr), sc:n(d.sourceOfInitialContact), rs:n(d.referralSource), + sp:n(d.salesPerson), ps:n(d.prospectStatus), lr:n(d.lostReason), bim:n(d.basicInterestMake), + bimd:n(d.basicInterestModel), nu:n(d.newUsed), dc:n(d.dtProspectCreated), + du:n(d.dtProspectUpdated), da:n(d.dtLastAction), bid:bid }, + pairedItem: { item: it.index } }); + if (d.statusLog) for (const s of d.statusLog) { + out.push({ json: { _t:'S', pid:d.prospectID, st:s.status, dt:n(s.dtUpdated), bid:bid }, + pairedItem: { item: it.index } }); + } +} +return out;` } } }); + +const insertP = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Insert Prospects', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospects (prospect_id,contact_id,cms_dealer_ref,keyloop_cust_id,keyloop_cust_acc_number,dealer_desc,company_name,title,name,surname,dt_birth,language,cell_number,email_addr,source_of_initial_contact,referral_source,sales_person,prospect_status,lost_reason,basic_interest_make,basic_interest_model,new_used,dt_prospect_created,dt_prospect_updated,dt_last_action,import_batch_id) +VALUES ({{ $json.pid }},{{ $json.cid || 'NULL' }},{{ $json.cdr ? "'"+$json.cdr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.kci ? "'"+$json.kci+"'" : 'NULL' }},{{ $json.kca ? "'"+$json.kca+"'" : 'NULL' }},{{ $json.dd ? "'"+$json.dd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.cn ? "'"+$json.cn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ti ? "'"+$json.ti.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nm ? "'"+$json.nm.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.db ? "'"+$json.db+"'::date" : 'NULL' }},{{ $json.lg ? "'"+$json.lg+"'" : 'NULL' }},{{ $json.cl ? "'"+$json.cl+"'" : 'NULL' }},{{ $json.em ? "'"+$json.em+"'" : 'NULL' }},{{ $json.sc ? "'"+$json.sc.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.rs ? "'"+$json.rs.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sp ? "'"+$json.sp.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ps ? "'"+$json.ps.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.lr ? "'"+$json.lr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bim ? "'"+$json.bim.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bimd ? "'"+$json.bimd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nu ? "'"+$json.nu+"'" : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.du ? "'"+$json.du+"'::timestamptz" : 'NULL' }},{{ $json.da ? "'"+$json.da+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertS = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Insert Status Logs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO status_logs (prospect_id,status,dt_updated,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.st ? "'"+$json.st.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.dt ? "'"+$json.dt+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id,status,dt_updated) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +export default workflow('CMS_BI_PROSPECTS_CREATED', 'CMS BI: GetProspectsByDateCreated → jetour-bi') + .add(cron).to(auth).to(setVars).to(apiCall).to(flatten) + .add(flatten).to(insertP).add(flatten).to(insertS); diff --git a/n8n/wf2_prospects_updated.ts b/n8n/wf2_prospects_updated.ts new file mode 100644 index 0000000..b34897c --- /dev/null +++ b/n8n/wf2_prospects_updated.ts @@ -0,0 +1,71 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Daily 02:10', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 10 2 * * *' }] } } } }); + +const auth = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Auth CMS BI', parameters: { method: 'POST', url: 'https://aws-prod-auth.cms360.cloud/api/Jwt/FromBody', + sendBody: true, bodyParameters: { parameters: [{ name: 'username', value: 'JetourMauritiusBI' }, { name: 'password', value: 'j7PW0ev66mJ2' }] }, options: { timeout: 30000 } } } }); + +const setVars = node({ type: 'n8n-nodes-base.set', version: 3.4, + config: { name: 'Set Dates + Token', parameters: { mode: 'manual', includeOtherFields: false, + assignments: { assignments: [ + { id: 't', name: 'token', value: expr('={{ $json.token }}'), type: 'string' }, + { id: 'f', name: 'from', value: expr('={{ DateTime.now().minus({ days: 1 }).startOf("day").toISO() }}'), type: 'string' }, + { id: 't2', name: 'to', value: expr('={{ DateTime.now().startOf("day").toISO() }}'), type: 'string' } + ] } } } }); + +const apiCall = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Fetch Prospects', parameters: { method: 'POST', + url: 'https://bi-api.cmscloud.co.za/api/BusinessIntelligence/Prospects/GetProspectsByDateUpdated', + sendBody: true, sendHeaders: true, specifyHeaders: 'keypair', + headerParameters: { parameters: [ + { name: 'Authorization', value: expr('=Bearer {{ $("Set Dates + Token").item.json.token }}') }, + { name: 'Content-Type', value: 'application/json' }] }, + bodyParameters: { parameters: [ + { id: 'f', name: 'from', value: expr('={{ $("Set Dates + Token").item.json.from }}') }, + { id: 't2', name: 'to', value: expr('={{ $("Set Dates + Token").item.json.to }}') }, + { name: 'pageNumber', value: '1' }, { name: 'pageSize', value: '500' }] }, + options: { timeout: 120000 } } } }); + +const flatten = node({ type: 'n8n-nodes-base.code', version: 2, + config: { name: 'Flatten', parameters: { mode: 'runOnceForAllItems', jsCode: +`const items = $input.all(); +const ts = new Date().toISOString().replace(/[-:T]/g,'').slice(0,15); +const bid = "pr_upd_" + ts; +const out = []; +function n(v) { return v || null; } +for (const it of items) { + const d = it.json; + out.push({ json: { _t:'P', pid:d.prospectID, cid:d.contactID, cdr:n(d.cmsDealerRef), kci:n(d.keyloopCustID), + kca:n(d.keyloopCustAccNumber), dd:n(d.dealerDesc), cn:n(d.companyName), ti:n(d.title), + nm:n(d.name), sn:n(d.surname), db:n(d.dtBirth), lg:n(d.language), cl:n(d.cellNumber), + em:n(d.emailAddr), sc:n(d.sourceOfInitialContact), rs:n(d.referralSource), + sp:n(d.salesPerson), ps:n(d.prospectStatus), lr:n(d.lostReason), bim:n(d.basicInterestMake), + bimd:n(d.basicInterestModel), nu:n(d.newUsed), dc:n(d.dtProspectCreated), + du:n(d.dtProspectUpdated), da:n(d.dtLastAction), bid:bid }, + pairedItem: { item: it.index } }); + if (d.statusLog) for (const s of d.statusLog) { + out.push({ json: { _t:'S', pid:d.prospectID, st:s.status, dt:n(s.dtUpdated), bid:bid }, + pairedItem: { item: it.index } }); + } +} +return out;` } } }); + +const insertP = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Insert Prospects', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospects (prospect_id,contact_id,cms_dealer_ref,keyloop_cust_id,keyloop_cust_acc_number,dealer_desc,company_name,title,name,surname,dt_birth,language,cell_number,email_addr,source_of_initial_contact,referral_source,sales_person,prospect_status,lost_reason,basic_interest_make,basic_interest_model,new_used,dt_prospect_created,dt_prospect_updated,dt_last_action,import_batch_id) +VALUES ({{ $json.pid }},{{ $json.cid || 'NULL' }},{{ $json.cdr ? "'"+$json.cdr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.kci ? "'"+$json.kci+"'" : 'NULL' }},{{ $json.kca ? "'"+$json.kca+"'" : 'NULL' }},{{ $json.dd ? "'"+$json.dd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.cn ? "'"+$json.cn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ti ? "'"+$json.ti.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nm ? "'"+$json.nm.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.db ? "'"+$json.db+"'::date" : 'NULL' }},{{ $json.lg ? "'"+$json.lg+"'" : 'NULL' }},{{ $json.cl ? "'"+$json.cl+"'" : 'NULL' }},{{ $json.em ? "'"+$json.em+"'" : 'NULL' }},{{ $json.sc ? "'"+$json.sc.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.rs ? "'"+$json.rs.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sp ? "'"+$json.sp.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ps ? "'"+$json.ps.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.lr ? "'"+$json.lr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bim ? "'"+$json.bim.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bimd ? "'"+$json.bimd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nu ? "'"+$json.nu+"'" : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.du ? "'"+$json.du+"'::timestamptz" : 'NULL' }},{{ $json.da ? "'"+$json.da+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id) DO UPDATE SET contact_id=EXCLUDED.contact_id, cms_dealer_ref=EXCLUDED.cms_dealer_ref, dealer_desc=EXCLUDED.dealer_desc, company_name=EXCLUDED.company_name, title=EXCLUDED.title, name=EXCLUDED.name, surname=EXCLUDED.surname, cell_number=EXCLUDED.cell_number, email_addr=EXCLUDED.email_addr, sales_person=EXCLUDED.sales_person, prospect_status=EXCLUDED.prospect_status, lost_reason=EXCLUDED.lost_reason, basic_interest_make=EXCLUDED.basic_interest_make, basic_interest_model=EXCLUDED.basic_interest_model, dt_prospect_updated=EXCLUDED.dt_prospect_updated, dt_last_action=EXCLUDED.dt_last_action, last_imported_at=NOW(), import_batch_id=EXCLUDED.import_batch_id;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertS = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Insert Status Logs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO status_logs (prospect_id,status,dt_updated,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.st ? "'"+$json.st.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.dt ? "'"+$json.dt+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id,status,dt_updated) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +export default workflow('CMS_BI_PROSPECTS_UPDATED', 'CMS BI: GetProspectsByDateUpdated → jetour-bi') + .add(cron).to(auth).to(setVars).to(apiCall).to(flatten) + .add(flatten).to(insertP).add(flatten).to(insertS); diff --git a/n8n/wf3_otps_created.ts b/n8n/wf3_otps_created.ts new file mode 100644 index 0000000..ca16fc4 --- /dev/null +++ b/n8n/wf3_otps_created.ts @@ -0,0 +1,86 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Daily 02:20', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '20 2 * * *' }] } } } }); + +const auth = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Auth CMS BI', parameters: { method: 'POST', url: 'https://aws-prod-auth.cms360.cloud/api/Jwt/FromBody', + sendBody: true, bodyParameters: { parameters: [{ name: 'username', value: 'JetourMauritiusBI' }, { name: 'password', value: 'j7PW0ev66mJ2' }] }, options: { timeout: 30000 } } } }); + +const setVars = node({ type: 'n8n-nodes-base.set', version: 3.4, + config: { name: 'Set Dates + Token', parameters: { mode: 'manual', includeOtherFields: false, + assignments: { assignments: [ + { id: 't', name: 'token', value: expr('={{ $json.token }}'), type: 'string' }, + { id: 'f', name: 'from', value: expr('={{ DateTime.now().minus({ days: 1 }).startOf("day").toISO() }}'), type: 'string' }, + { id: 't2', name: 'to', value: expr('={{ DateTime.now().startOf("day").toISO() }}'), type: 'string' } + ] } } } }); + +const apiCall = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Fetch OTPs', parameters: { method: 'POST', + url: 'https://bi-api.cmscloud.co.za/api/BusinessIntelligence/Otps/GetOtpsByDateCreated', + sendBody: true, sendHeaders: true, specifyHeaders: 'keypair', + headerParameters: { parameters: [ + { name: 'Authorization', value: expr('=Bearer {{ $("Set Dates + Token").item.json.token }}') }, + { name: 'Content-Type', value: 'application/json' }] }, + bodyParameters: { parameters: [ + { id: 'f', name: 'from', value: expr('={{ $("Set Dates + Token").item.json.from }}') }, + { id: 't2', name: 'to', value: expr('={{ $("Set Dates + Token").item.json.to }}') }, + { name: 'pageNumber', value: '1' }, { name: 'pageSize', value: '500' }] }, + options: { timeout: 120000 } } } }); + +const flatten = node({ type: 'n8n-nodes-base.code', version: 2, + config: { name: 'Flatten', parameters: { mode: 'runOnceForAllItems', jsCode: +`const items = $input.all(); +const ts = new Date().toISOString().replace(/[-:T]/g,'').slice(0,15); +const bid = "ot_cr_" + ts; +const out = []; +function n(v) { return v || null; } +function pu(v) { if(!v) return null; return v.replace(/[{}]/g,'')||null; } +for (const it of items) { + const d = it.json; + out.push({ json: { _t:'P', pid:d.prospectID, cid:d.contactID, cdr:n(d.cmsDealerRef), kci:n(d.keyloopCustID), + kca:n(d.keyloopCustAccNumber), dd:n(d.dealerDesc), cn:n(d.companyName), ti:n(d.title), + nm:n(d.name), sn:n(d.surname), db:n(d.dtBirth), lg:n(d.language), cl:n(d.cellNumber), + em:n(d.emailAddr), sc:n(d.sourceOfInitialContact), rs:n(d.referralSource), + sp:n(d.salesPerson), ps:n(d.prospectStatus), lr:n(d.lostReason), bim:n(d.basicInterestMake), + bimd:n(d.basicInterestModel), nu:n(d.newUsed), dc:n(d.dtProspectCreated), + du:n(d.dtProspectUpdated), da:n(d.dtLastAction), bid:bid }, + pairedItem: { item: it.index } }); + if (d.prospectOTPS) for (const o of d.prospectOTPS) { + out.push({ json: { _t:'O', pid:d.prospectID, oid:o.cmsotpid, cpid:o.cmsProspectID, + eg:pu(o.exportGUID), on:n(o.otpNewUsed), mc:n(o.mmCode), mk:n(o.make), md:n(o.model), + mr:n(o.modelRange), co:n(o.colour), my:n(o.modelYear), vi:n(o.vin), sn:n(o.stockNo), + am:o.amount, dc:n(o.dtotpCreated), ds:n(o.dtSold), di:n(o.dtInvoiced), bid:bid }, + pairedItem: { item: it.index } }); + } + if (d.statusLog) for (const s of d.statusLog) { + out.push({ json: { _t:'S', pid:d.prospectID, st:s.status, dt:n(s.dtUpdated), bid:bid }, + pairedItem: { item: it.index } }); + } +} +return out;` } } }); + +const insertP = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert Prospects', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospects (prospect_id,contact_id,cms_dealer_ref,keyloop_cust_id,keyloop_cust_acc_number,dealer_desc,company_name,title,name,surname,dt_birth,language,cell_number,email_addr,source_of_initial_contact,referral_source,sales_person,prospect_status,lost_reason,basic_interest_make,basic_interest_model,new_used,dt_prospect_created,dt_prospect_updated,dt_last_action,import_batch_id) +VALUES ({{ $json.pid }},{{ $json.cid || 'NULL' }},{{ $json.cdr ? "'"+$json.cdr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.kci ? "'"+$json.kci+"'" : 'NULL' }},{{ $json.kca ? "'"+$json.kca+"'" : 'NULL' }},{{ $json.dd ? "'"+$json.dd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.cn ? "'"+$json.cn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ti ? "'"+$json.ti.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nm ? "'"+$json.nm.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.db ? "'"+$json.db+"'::date" : 'NULL' }},{{ $json.lg ? "'"+$json.lg+"'" : 'NULL' }},{{ $json.cl ? "'"+$json.cl+"'" : 'NULL' }},{{ $json.em ? "'"+$json.em+"'" : 'NULL' }},{{ $json.sc ? "'"+$json.sc.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.rs ? "'"+$json.rs.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sp ? "'"+$json.sp.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ps ? "'"+$json.ps.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.lr ? "'"+$json.lr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bim ? "'"+$json.bim.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bimd ? "'"+$json.bimd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nu ? "'"+$json.nu+"'" : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.du ? "'"+$json.du+"'::timestamptz" : 'NULL' }},{{ $json.da ? "'"+$json.da+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertO = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert OTPs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospect_otps (prospect_id,cms_otp_id,cms_prospect_id,export_guid,otp_new_used,mm_code,make,model,model_range,colour,model_year,vin,stock_no,amount,dt_otp_created,dt_sold,dt_invoiced,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.oid }},{{ $json.cpid }},{{ $json.eg ? "'"+$json.eg+"'::uuid" : 'NULL' }},{{ $json.on ? "'"+$json.on+"'" : 'NULL' }},{{ $json.mc ? "'"+$json.mc+"'" : 'NULL' }},{{ $json.mk ? "'"+$json.mk.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.md ? "'"+$json.md.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.mr ? "'"+$json.mr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.co ? "'"+$json.co.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.my ? "'"+$json.my+"'" : 'NULL' }},{{ $json.vi ? "'"+$json.vi+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.am !== null && $json.am !== undefined ? $json.am : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.ds ? "'"+$json.ds+"'::timestamptz" : 'NULL' }},{{ $json.di ? "'"+$json.di+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (cms_otp_id) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertS = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert Status Logs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO status_logs (prospect_id,status,dt_updated,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.st ? "'"+$json.st.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.dt ? "'"+$json.dt+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id,status,dt_updated) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +export default workflow('CMS_BI_OTPS_CREATED', 'CMS BI: GetOtpsByDateCreated → jetour-bi') + .add(cron).to(auth).to(setVars).to(apiCall).to(flatten) + .add(flatten).to(insertP).add(flatten).to(insertO).add(flatten).to(insertS); diff --git a/n8n/wf4_otps_updated.ts b/n8n/wf4_otps_updated.ts new file mode 100644 index 0000000..cdb7246 --- /dev/null +++ b/n8n/wf4_otps_updated.ts @@ -0,0 +1,86 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Daily 02:50', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '50 2 * * *' }] } } } }); + +const auth = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Auth CMS BI', parameters: { method: 'POST', url: 'https://aws-prod-auth.cms360.cloud/api/Jwt/FromBody', + sendBody: true, bodyParameters: { parameters: [{ name: 'username', value: 'JetourMauritiusBI' }, { name: 'password', value: 'j7PW0ev66mJ2' }] }, options: { timeout: 30000 } } } }); + +const setVars = node({ type: 'n8n-nodes-base.set', version: 3.4, + config: { name: 'Set Dates + Token', parameters: { mode: 'manual', includeOtherFields: false, + assignments: { assignments: [ + { id: 't', name: 'token', value: expr('={{ $json.token }}'), type: 'string' }, + { id: 'f', name: 'from', value: expr('={{ DateTime.now().minus({ days: 1 }).startOf("day").toISO() }}'), type: 'string' }, + { id: 't2', name: 'to', value: expr('={{ DateTime.now().startOf("day").toISO() }}'), type: 'string' } + ] } } } }); + +const apiCall = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Fetch OTPs', parameters: { method: 'POST', + url: 'https://bi-api.cmscloud.co.za/api/BusinessIntelligence/Otps/GetOtpsByDateUpdated', + sendBody: true, sendHeaders: true, specifyHeaders: 'keypair', + headerParameters: { parameters: [ + { name: 'Authorization', value: expr('=Bearer {{ $("Set Dates + Token").item.json.token }}') }, + { name: 'Content-Type', value: 'application/json' }] }, + bodyParameters: { parameters: [ + { id: 'f', name: 'from', value: expr('={{ $("Set Dates + Token").item.json.from }}') }, + { id: 't2', name: 'to', value: expr('={{ $("Set Dates + Token").item.json.to }}') }, + { name: 'pageNumber', value: '1' }, { name: 'pageSize', value: '500' }] }, + options: { timeout: 120000 } } } }); + +const flatten = node({ type: 'n8n-nodes-base.code', version: 2, + config: { name: 'Flatten', parameters: { mode: 'runOnceForAllItems', jsCode: +`const items = $input.all(); +const ts = new Date().toISOString().replace(/[-:T]/g,'').slice(0,15); +const bid = "ot_upd_" + ts; +const out = []; +function n(v) { return v || null; } +function pu(v) { if(!v) return null; return v.replace(/[{}]/g,'')||null; } +for (const it of items) { + const d = it.json; + out.push({ json: { _t:'P', pid:d.prospectID, cid:d.contactID, cdr:n(d.cmsDealerRef), kci:n(d.keyloopCustID), + kca:n(d.keyloopCustAccNumber), dd:n(d.dealerDesc), cn:n(d.companyName), ti:n(d.title), + nm:n(d.name), sn:n(d.surname), db:n(d.dtBirth), lg:n(d.language), cl:n(d.cellNumber), + em:n(d.emailAddr), sc:n(d.sourceOfInitialContact), rs:n(d.referralSource), + sp:n(d.salesPerson), ps:n(d.prospectStatus), lr:n(d.lostReason), bim:n(d.basicInterestMake), + bimd:n(d.basicInterestModel), nu:n(d.newUsed), dc:n(d.dtProspectCreated), + du:n(d.dtProspectUpdated), da:n(d.dtLastAction), bid:bid }, + pairedItem: { item: it.index } }); + if (d.prospectOTPS) for (const o of d.prospectOTPS) { + out.push({ json: { _t:'O', pid:d.prospectID, oid:o.cmsotpid, cpid:o.cmsProspectID, + eg:pu(o.exportGUID), on:n(o.otpNewUsed), mc:n(o.mmCode), mk:n(o.make), md:n(o.model), + mr:n(o.modelRange), co:n(o.colour), my:n(o.modelYear), vi:n(o.vin), sn:n(o.stockNo), + am:o.amount, dc:n(o.dtotpCreated), ds:n(o.dtSold), di:n(o.dtInvoiced), bid:bid }, + pairedItem: { item: it.index } }); + } + if (d.statusLog) for (const s of d.statusLog) { + out.push({ json: { _t:'S', pid:d.prospectID, st:s.status, dt:n(s.dtUpdated), bid:bid }, + pairedItem: { item: it.index } }); + } +} +return out;` } } }); + +const insertP = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert Prospects', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospects (prospect_id,contact_id,cms_dealer_ref,keyloop_cust_id,keyloop_cust_acc_number,dealer_desc,company_name,title,name,surname,dt_birth,language,cell_number,email_addr,source_of_initial_contact,referral_source,sales_person,prospect_status,lost_reason,basic_interest_make,basic_interest_model,new_used,dt_prospect_created,dt_prospect_updated,dt_last_action,import_batch_id) +VALUES ({{ $json.pid }},{{ $json.cid || 'NULL' }},{{ $json.cdr ? "'"+$json.cdr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.kci ? "'"+$json.kci+"'" : 'NULL' }},{{ $json.kca ? "'"+$json.kca+"'" : 'NULL' }},{{ $json.dd ? "'"+$json.dd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.cn ? "'"+$json.cn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ti ? "'"+$json.ti.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nm ? "'"+$json.nm.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.db ? "'"+$json.db+"'::date" : 'NULL' }},{{ $json.lg ? "'"+$json.lg+"'" : 'NULL' }},{{ $json.cl ? "'"+$json.cl+"'" : 'NULL' }},{{ $json.em ? "'"+$json.em+"'" : 'NULL' }},{{ $json.sc ? "'"+$json.sc.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.rs ? "'"+$json.rs.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.sp ? "'"+$json.sp.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.ps ? "'"+$json.ps.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.lr ? "'"+$json.lr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bim ? "'"+$json.bim.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.bimd ? "'"+$json.bimd.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.nu ? "'"+$json.nu+"'" : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.du ? "'"+$json.du+"'::timestamptz" : 'NULL' }},{{ $json.da ? "'"+$json.da+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertO = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert OTPs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO prospect_otps (prospect_id,cms_otp_id,cms_prospect_id,export_guid,otp_new_used,mm_code,make,model,model_range,colour,model_year,vin,stock_no,amount,dt_otp_created,dt_sold,dt_invoiced,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.oid }},{{ $json.cpid }},{{ $json.eg ? "'"+$json.eg+"'::uuid" : 'NULL' }},{{ $json.on ? "'"+$json.on+"'" : 'NULL' }},{{ $json.mc ? "'"+$json.mc+"'" : 'NULL' }},{{ $json.mk ? "'"+$json.mk.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.md ? "'"+$json.md.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.mr ? "'"+$json.mr.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.co ? "'"+$json.co.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.my ? "'"+$json.my+"'" : 'NULL' }},{{ $json.vi ? "'"+$json.vi+"'" : 'NULL' }},{{ $json.sn ? "'"+$json.sn.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.am !== null && $json.am !== undefined ? $json.am : 'NULL' }},{{ $json.dc ? "'"+$json.dc+"'::timestamptz" : 'NULL' }},{{ $json.ds ? "'"+$json.ds+"'::timestamptz" : 'NULL' }},{{ $json.di ? "'"+$json.di+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (cms_otp_id) DO UPDATE SET prospect_id=EXCLUDED.prospect_id, cms_prospect_id=EXCLUDED.cms_prospect_id, export_guid=EXCLUDED.export_guid, otp_new_used=EXCLUDED.otp_new_used, mm_code=EXCLUDED.mm_code, make=EXCLUDED.make, model=EXCLUDED.model, model_range=EXCLUDED.model_range, colour=EXCLUDED.colour, model_year=EXCLUDED.model_year, vin=EXCLUDED.vin, stock_no=EXCLUDED.stock_no, amount=EXCLUDED.amount, dt_sold=EXCLUDED.dt_sold, dt_invoiced=EXCLUDED.dt_invoiced, last_imported_at=NOW(), import_batch_id=EXCLUDED.import_batch_id;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +const insertS = node({ type: 'n8n-nodes-base.postgres', version: 2.4, + config: { name: 'Upsert Status Logs', parameters: { operation: 'executeQuery', query: + expr(`=INSERT INTO status_logs (prospect_id,status,dt_updated,import_batch_id) +VALUES ((SELECT id FROM prospects WHERE prospect_id={{ $json.pid }}),{{ $json.st ? "'"+$json.st.replace(/'/g,"''")+"'" : 'NULL' }},{{ $json.dt ? "'"+$json.dt+"'::timestamptz" : 'NULL' }},{{ "'"+$json.bid+"'" }}) +ON CONFLICT (prospect_id,status,dt_updated) DO NOTHING;`) }, + credentials: { postgres: { name: 'jetour-bi-remote' } } } }); + +export default workflow('CMS_BI_OTPS_UPDATED', 'CMS BI: GetOtpsByDateUpdated → jetour-bi') + .add(cron).to(auth).to(setVars).to(apiCall).to(flatten) + .add(flatten).to(insertP).add(flatten).to(insertO).add(flatten).to(insertS); diff --git a/n8n/wf_email_daily.ts b/n8n/wf_email_daily.ts new file mode 100644 index 0000000..41bc09b --- /dev/null +++ b/n8n/wf_email_daily.ts @@ -0,0 +1,14 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Daily 05:00 UTC', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 5 * * *' }] } } } }); + +const sendReport = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Send Daily Report', parameters: { + method: 'POST', url: 'http://100.91.25.139:8765/api/send-report/daily', + sendBody: false, sendHeaders: false, + options: { timeout: 60000, redirect: { redirect: {} }, response: { response: { responseFormat: 'json' } } } + } } }); + +export default workflow('CMS_BI_EMAIL_DAILY', '📧 CMS BI: Daily Email Report') + .add(cron).to(sendReport); diff --git a/n8n/wf_email_monthly.ts b/n8n/wf_email_monthly.ts new file mode 100644 index 0000000..9550dae --- /dev/null +++ b/n8n/wf_email_monthly.ts @@ -0,0 +1,14 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Monthly 1st 05:00 UTC', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 5 1 * *' }] } } } }); + +const sendReport = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Send Monthly Report', parameters: { + method: 'POST', url: 'http://100.91.25.139:8765/api/send-report/monthly', + sendBody: false, sendHeaders: false, + options: { timeout: 60000, redirect: { redirect: {} }, response: { response: { responseFormat: 'json' } } } + } } }); + +export default workflow('CMS_BI_EMAIL_MONTHLY', '📧 CMS BI: Monthly Email Report') + .add(cron).to(sendReport); diff --git a/n8n/wf_email_weekly.ts b/n8n/wf_email_weekly.ts new file mode 100644 index 0000000..1652820 --- /dev/null +++ b/n8n/wf_email_weekly.ts @@ -0,0 +1,14 @@ +import { workflow, node, trigger, expr } from '@n8n/workflow-sdk'; + +const cron = trigger({ type: 'n8n-nodes-base.scheduleTrigger', version: 1.3, + config: { name: 'Weekly Mon 05:00 UTC', parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 5 * * 1' }] } } } }); + +const sendReport = node({ type: 'n8n-nodes-base.httpRequest', version: 4.4, + config: { name: 'Send Weekly Report', parameters: { + method: 'POST', url: 'http://100.91.25.139:8765/api/send-report/weekly', + sendBody: false, sendHeaders: false, + options: { timeout: 60000, redirect: { redirect: {} }, response: { response: { responseFormat: 'json' } } } + } } }); + +export default workflow('CMS_BI_EMAIL_WEEKLY', '📧 CMS BI: Weekly Email Report') + .add(cron).to(sendReport);