feat: add Mietvertrag workflow and template management in n8n, including email notifications and document handling
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -34,3 +34,7 @@
|
||||
{"__type":"$$EventMessageConfirm","confirm":"eb33d71c-ab1e-45e7-b8a1-c2f4233db4cc","ts":"2026-04-29T20:02:02.388+02:00","source":{"id":"0","name":"eventBus"}}
|
||||
{"__type":"$$EventMessageWorkflow","id":"0da331f2-66a0-45fd-b691-d4c28a01598c","ts":"2026-04-29T20:02:02.404+02:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"aeec9612-50f4-4124-889e-83325fb5af8a","executionId":"2","success":true,"isManual":true,"mode":"manual","workflowId":"MiEBjJYxhUVm6sl3","workflowName":"New Lead Notification","projectId":"julyz3yhfTK4pMNd","projectName":"MC Cars <admin@mccars.local>"}}
|
||||
{"__type":"$$EventMessageConfirm","confirm":"0da331f2-66a0-45fd-b691-d4c28a01598c","ts":"2026-04-29T20:02:02.404+02:00","source":{"id":"0","name":"eventBus"}}
|
||||
{"__type":"$$EventMessageAudit","id":"a0ac56d7-5dd2-4d92-ba16-9480f5d7be4e","ts":"2026-04-29T21:40:47.656+02:00","eventName":"n8n.audit.workflow.created","message":"n8n.audit.workflow.created","payload":{"userId":"aeec9612-50f4-4124-889e-83325fb5af8a","_email":"admin@mccars.local","_firstName":"MC","_lastName":"Cars","globalRole":"global:owner","workflowId":"5254P6wnxXA4bhWf","workflowName":"Lead Qualified → Payment Email"}}
|
||||
{"__type":"$$EventMessageConfirm","confirm":"a0ac56d7-5dd2-4d92-ba16-9480f5d7be4e","ts":"2026-04-29T21:40:47.656+02:00","source":{"id":"0","name":"eventBus"}}
|
||||
{"__type":"$$EventMessageAudit","id":"8b319e39-54c7-41d6-a1f6-4a28fcc9f340","ts":"2026-04-29T21:41:00.236+02:00","eventName":"n8n.audit.workflow.created","message":"n8n.audit.workflow.created","payload":{"userId":"aeec9612-50f4-4124-889e-83325fb5af8a","_email":"admin@mccars.local","_firstName":"MC","_lastName":"Cars","globalRole":"global:owner","workflowId":"40NOffvLouMXkVPO","workflowName":"Lead Qualified → Mietvertrag PDF"}}
|
||||
{"__type":"$$EventMessageConfirm","confirm":"8b319e39-54c7-41d6-a1f6-4a28fcc9f340","ts":"2026-04-29T21:41:00.236+02:00","source":{"id":"0","name":"eventBus"}}
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
- ./supabase/migrations/07-sales-orders.sql:/sql/07-sales-orders.sql:ro
|
||||
- ./supabase/migrations/08-backend-pricing-and-security.sql:/sql/08-backend-pricing-and-security.sql:ro
|
||||
- ./supabase/migrations/09-site-settings.sql:/sql/09-site-settings.sql:ro
|
||||
- ./supabase/migrations/10-mietvertrag-workflow.sql:/sql/10-mietvertrag-workflow.sql:ro
|
||||
|
||||
kong:
|
||||
volumes:
|
||||
|
||||
@@ -217,6 +217,7 @@ services:
|
||||
- /mnt/user/appdata/mc-cars/supabase/migrations/07-sales-orders.sql:/sql/07-sales-orders.sql:ro
|
||||
- /mnt/user/appdata/mc-cars/supabase/migrations/08-backend-pricing-and-security.sql:/sql/08-backend-pricing-and-security.sql:ro
|
||||
- /mnt/user/appdata/mc-cars/supabase/migrations/09-site-settings.sql:/sql/09-site-settings.sql:ro
|
||||
- /mnt/user/appdata/mc-cars/supabase/migrations/10-mietvertrag-workflow.sql:/sql/10-mietvertrag-workflow.sql:ro
|
||||
entrypoint: ["sh","-c"]
|
||||
command:
|
||||
- |
|
||||
@@ -242,6 +243,7 @@ services:
|
||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/07-sales-orders.sql
|
||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/08-backend-pricing-and-security.sql
|
||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/09-site-settings.sql
|
||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/10-mietvertrag-workflow.sql
|
||||
echo "post-init done."
|
||||
restart: "no"
|
||||
networks: [mccars]
|
||||
@@ -413,3 +415,19 @@ services:
|
||||
- "55590:5678"
|
||||
networks: [mccars]
|
||||
logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Gotenberg - headless document converter (DOCX → PDF)
|
||||
# Used by n8n to generate Mietvertrag PDFs from DOCX templates.
|
||||
# -------------------------------------------------------------------------
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
container_name: mccars-gotenberg
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--api-port=3000"
|
||||
- "--api-timeout=60s"
|
||||
- "--libreoffice-restart-after=10"
|
||||
networks: [mccars]
|
||||
logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }
|
||||
|
||||
@@ -253,6 +253,16 @@
|
||||
<p class="muted" style="font-size:0.82rem;" data-i18n="adminHeroImageHint">JPG/PNG/WebP. Wird als Hintergrundbild im Hero-Bereich der Website angezeigt.</p>
|
||||
<p class="form-feedback" id="heroFeedback"></p>
|
||||
</div>
|
||||
<hr style="margin:2rem 0;border-color:var(--border);" />
|
||||
<div class="admin-form">
|
||||
<label>
|
||||
<span data-i18n="adminMietvertragTemplate">Mietvertrag-Vorlage (DOCX)</span>
|
||||
<div id="mietvertragStatus" class="muted" style="font-size:0.9rem;margin:0.5rem 0;"></div>
|
||||
<input type="file" id="mietvertragInput" accept="application/vnd.openxmlformats-officedocument.wordprocessingml.document,.docx" />
|
||||
</label>
|
||||
<p class="muted" style="font-size:0.82rem;" data-i18n="adminMietvertragHint">DOCX-Vorlage mit Platzhaltern. Wird bei Qualifizierung automatisch ausgefüllt und als PDF per E-Mail versendet.</p>
|
||||
<p class="form-feedback" id="mietvertragFeedback"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1108,11 +1108,25 @@ orderDialog.addEventListener("close", onDialogClose);
|
||||
const heroPreview = document.querySelector("#heroPreview");
|
||||
const heroImageInput = document.querySelector("#heroImageInput");
|
||||
const heroFeedback = document.querySelector("#heroFeedback");
|
||||
const mietvertragStatus = document.querySelector("#mietvertragStatus");
|
||||
const mietvertragInput = document.querySelector("#mietvertragInput");
|
||||
const mietvertragFeedback = document.querySelector("#mietvertragFeedback");
|
||||
|
||||
async function renderSettings() {
|
||||
const { data } = await supabase.from("site_settings").select("value").eq("key", "hero_image_url").single();
|
||||
const url = data?.value || "/images/ferrari-main-car.png";
|
||||
heroPreview.style.backgroundImage = `url('${url}')`;
|
||||
|
||||
// Mietvertrag template status
|
||||
const { data: tplData } = await supabase.from("site_settings").select("value").eq("key", "mietvertrag_template_path").single();
|
||||
const tplPath = tplData?.value;
|
||||
if (tplPath) {
|
||||
mietvertragStatus.textContent = "✓ " + tplPath.split("/").pop();
|
||||
mietvertragStatus.style.color = "var(--success, green)";
|
||||
} else {
|
||||
mietvertragStatus.textContent = t("adminMietvertragEmpty") || "Keine Vorlage hochgeladen.";
|
||||
mietvertragStatus.style.color = "var(--muted)";
|
||||
}
|
||||
}
|
||||
|
||||
heroImageInput.addEventListener("change", async () => {
|
||||
@@ -1145,4 +1159,32 @@ heroImageInput.addEventListener("change", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
mietvertragInput.addEventListener("change", async () => {
|
||||
const file = mietvertragInput.files?.[0];
|
||||
if (!file) return;
|
||||
mietvertragFeedback.className = "form-feedback";
|
||||
mietvertragFeedback.textContent = "Uploading...";
|
||||
try {
|
||||
const path = `mietvertrag/vorlage.docx`;
|
||||
const { error: upErr } = await supabase.storage
|
||||
.from("document-templates")
|
||||
.upload(path, file, { contentType: file.type, upsert: true });
|
||||
if (upErr) throw upErr;
|
||||
|
||||
// Save path to site_settings
|
||||
const { error: dbErr } = await supabase
|
||||
.from("site_settings")
|
||||
.upsert({ key: "mietvertrag_template_path", value: path, updated_at: new Date().toISOString() }, { onConflict: "key" });
|
||||
if (dbErr) throw dbErr;
|
||||
|
||||
mietvertragStatus.textContent = "✓ " + file.name;
|
||||
mietvertragStatus.style.color = "var(--success, green)";
|
||||
mietvertragFeedback.className = "form-feedback";
|
||||
mietvertragFeedback.textContent = "Gespeichert.";
|
||||
} catch (err) {
|
||||
mietvertragFeedback.className = "form-feedback error";
|
||||
mietvertragFeedback.textContent = err.message || String(err);
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -133,6 +133,9 @@ export const translations = {
|
||||
adminSettings: "Einstellungen",
|
||||
adminHeroImage: "Hauptbild (Hero-Bereich)",
|
||||
adminHeroImageHint: "JPG/PNG/WebP. Wird als Hintergrundbild im Hero-Bereich der Website angezeigt.",
|
||||
adminMietvertragTemplate: "Mietvertrag-Vorlage (DOCX)",
|
||||
adminMietvertragHint: "DOCX-Vorlage mit Platzhaltern. Wird bei Qualifizierung automatisch ausgefüllt und als PDF per E-Mail versendet.",
|
||||
adminMietvertragEmpty: "Keine Vorlage hochgeladen.",
|
||||
adminNewVehicle: "Neues Fahrzeug",
|
||||
adminAllVehicles: "Alle Fahrzeuge",
|
||||
adminPhotoUpload: "Foto hochladen (JPG/PNG/WebP, max 50 MB)",
|
||||
@@ -364,6 +367,9 @@ export const translations = {
|
||||
adminSettings: "Settings",
|
||||
adminHeroImage: "Main Photo (Hero Section)",
|
||||
adminHeroImageHint: "JPG/PNG/WebP. Displayed as the background image in the hero section of the website.",
|
||||
adminMietvertragTemplate: "Rental Contract Template (DOCX)",
|
||||
adminMietvertragHint: "DOCX template with placeholders. Automatically filled and sent as PDF via email upon qualification.",
|
||||
adminMietvertragEmpty: "No template uploaded.",
|
||||
adminNewVehicle: "New vehicle",
|
||||
adminAllVehicles: "All vehicles",
|
||||
adminPhotoUpload: "Upload photo (JPG/PNG/WebP, max 50 MB)",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "Lead Qualified → Payment Email",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"triggerOnNotify": true,
|
||||
"channel": "lead_qualified",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "pg-trigger",
|
||||
"name": "Postgres Trigger",
|
||||
"type": "n8n-nodes-base.postgresTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "1",
|
||||
"name": "MC Cars Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT c.id, c.name, c.email, c.phone,\n so.order_number, so.total_eur, so.deposit_eur,\n so.date_from, so.date_to, so.vehicle_label,\n so.daily_subtotal, so.weekend_subtotal,\n so.subtotal_eur, so.vat_eur,\n so.total_days, so.weekday_count, so.weekend_day_count\nFROM public.customers c\nJOIN public.sales_orders so ON so.customer_id = c.id\nWHERE so.id = '{{ $json.sales_order_id }}'::uuid",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "fetch-order-data",
|
||||
"name": "Fetch Order Data",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [470, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "1",
|
||||
"name": "MC Cars Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fromEmail": "info@mc-cars.at",
|
||||
"toEmail": "={{ $json.email }}",
|
||||
"subject": "MC Cars – Ihre Buchung {{ $json.order_number }} – Zahlungsanweisungen",
|
||||
"emailType": "html",
|
||||
"html": "<div style=\"font-family:Arial,sans-serif;max-width:600px;margin:0 auto;\">\n <div style=\"background:#1a1a1a;padding:20px;text-align:center;\">\n <h1 style=\"color:#c87941;margin:0;\">MC Cars</h1>\n </div>\n <div style=\"padding:30px;background:#f9f9f9;\">\n <p>Sehr geehrte/r <strong>{{ $json.name }}</strong>,</p>\n <p>vielen Dank für Ihre Buchung! Hier sind Ihre Buchungsdetails und Zahlungsanweisungen:</p>\n \n <table style=\"width:100%;border-collapse:collapse;margin:20px 0;\">\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Bestellnummer:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">{{ $json.order_number }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Fahrzeug:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">{{ $json.vehicle_label }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Zeitraum:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">{{ $json.date_from }} – {{ $json.date_to }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Tage gesamt:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">{{ $json.total_days }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Netto:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">€{{ $json.subtotal_eur }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>MwSt (19%):</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\">€{{ $json.vat_eur }}</td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Gesamtbetrag:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>€{{ $json.total_eur }}</strong></td></tr>\n <tr><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>Kaution:</strong></td><td style=\"padding:8px;border-bottom:1px solid #ddd;\"><strong>€{{ $json.deposit_eur }}</strong></td></tr>\n </table>\n\n <h3 style=\"color:#c87941;\">Zahlungsanweisungen</h3>\n \n <h4>1. Kaution (€{{ $json.deposit_eur }})</h4>\n <p>Bitte überweisen Sie die Kaution auf folgendes Konto:</p>\n <div style=\"background:#fff;padding:15px;border:1px solid #ddd;border-radius:5px;margin:10px 0;\">\n <p style=\"margin:5px 0;\"><strong>Empfänger:</strong> MC Cars GmbH</p>\n <p style=\"margin:5px 0;\"><strong>IBAN:</strong> AT00 0000 0000 0000 0000</p>\n <p style=\"margin:5px 0;\"><strong>BIC:</strong> BKAUATWW</p>\n <p style=\"margin:5px 0;\"><strong>Verwendungszweck:</strong> Kaution {{ $json.order_number }}</p>\n </div>\n\n <h4>2. Mietbetrag (€{{ $json.total_eur }})</h4>\n <p>Den Mietbetrag können Sie bequem online bezahlen:</p>\n <div style=\"text-align:center;margin:20px 0;\">\n <a href=\"https://mc-cars.at/zahlung/{{ $json.order_number }}\" style=\"background:#c87941;color:#fff;padding:12px 30px;text-decoration:none;border-radius:5px;font-weight:bold;\">Jetzt bezahlen</a>\n </div>\n <p style=\"font-size:0.85em;color:#666;\">Oder überweisen Sie auf dasselbe Konto mit Verwendungszweck: <strong>Miete {{ $json.order_number }}</strong></p>\n\n <hr style=\"border:none;border-top:1px solid #ddd;margin:25px 0;\" />\n <p>Bei Fragen stehen wir Ihnen jederzeit zur Verfügung.</p>\n <p>Mit freundlichen Grüßen,<br/><strong>MC Cars GmbH</strong><br/>info@mc-cars.at<br/>mc-cars.at</p>\n </div>\n</div>",
|
||||
"options": {}
|
||||
},
|
||||
"id": "send-email",
|
||||
"name": "Send Payment Email",
|
||||
"type": "n8n-nodes-base.emailSend",
|
||||
"typeVersion": 2.1,
|
||||
"position": [690, 300],
|
||||
"credentials": {
|
||||
"smtp": {
|
||||
"id": "2",
|
||||
"name": "MC Cars SMTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Postgres Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Order Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Fetch Order Data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Payment Email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [
|
||||
{
|
||||
"name": "mc-cars"
|
||||
}
|
||||
],
|
||||
"triggerCount": 1
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
{
|
||||
"name": "Lead Qualified → Mietvertrag PDF",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"triggerOnNotify": true,
|
||||
"channel": "lead_qualified",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "pg-trigger-mv",
|
||||
"name": "Postgres Trigger",
|
||||
"type": "n8n-nodes-base.postgresTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "1",
|
||||
"name": "MC Cars Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT value FROM public.site_settings WHERE key = 'mietvertrag_template_path'",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "check-template",
|
||||
"name": "Check Template Exists",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [470, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "1",
|
||||
"name": "MC Cars Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond1",
|
||||
"leftValue": "={{ $json.value }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "notEmpty"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "if-template-exists",
|
||||
"name": "Template Exists?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [670, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT c.name, c.email, c.phone,\n so.order_number, so.total_eur, so.deposit_eur,\n so.date_from, so.date_to, so.vehicle_label,\n so.daily_subtotal, so.weekend_subtotal,\n so.subtotal_eur, so.vat_eur,\n so.total_days, so.weekday_count, so.weekend_day_count,\n to_char(so.date_from, 'DD.MM.YYYY') as date_from_de,\n to_char(so.date_to, 'DD.MM.YYYY') as date_to_de,\n to_char(now(), 'DD.MM.YYYY') as today_de\nFROM public.customers c\nJOIN public.sales_orders so ON so.customer_id = c.id\nWHERE so.id = '{{ $('Postgres Trigger').item.json.sales_order_id }}'::uuid",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "fetch-full-data",
|
||||
"name": "Fetch Full Order+Customer",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [890, 200],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "1",
|
||||
"name": "MC Cars Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "=http://kong:8000/storage/v1/object/document-templates/{{ $('Check Template Exists').item.json.value }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "={{ $env.SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' }}"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "=Bearer {{ $env.SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "download-template",
|
||||
"name": "Download DOCX Template",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1110, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Fill DOCX template placeholders using simple text replacement.\n// The DOCX is a ZIP containing XML. We replace {{placeholder}} markers\n// in the document.xml with actual values from the order data.\n\nconst JSZip = require('jszip');\n\nconst binaryData = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst orderData = $('Fetch Full Order+Customer').first().json;\n\n// Placeholders map\nconst placeholders = {\n '{{KUNDE_NAME}}': orderData.name || '',\n '{{KUNDE_EMAIL}}': orderData.email || '',\n '{{KUNDE_TELEFON}}': orderData.phone || '',\n '{{BESTELLNUMMER}}': orderData.order_number || '',\n '{{FAHRZEUG}}': orderData.vehicle_label || '',\n '{{DATUM_VON}}': orderData.date_from_de || '',\n '{{DATUM_BIS}}': orderData.date_to_de || '',\n '{{TAGE_GESAMT}}': String(orderData.total_days || 0),\n '{{WOCHENTAGE}}': String(orderData.weekday_count || 0),\n '{{WOCHENENDTAGE}}': String(orderData.weekend_day_count || 0),\n '{{NETTO}}': String(orderData.subtotal_eur || 0),\n '{{MWST}}': String(orderData.vat_eur || 0),\n '{{GESAMT}}': String(orderData.total_eur || 0),\n '{{KAUTION}}': String(orderData.deposit_eur || 0),\n '{{TAGESSATZ}}': String(orderData.daily_subtotal || 0),\n '{{WOCHENENDZUSCHLAG}}': String(orderData.weekend_subtotal || 0),\n '{{DATUM_HEUTE}}': orderData.today_de || '',\n};\n\nconst zip = await JSZip.loadAsync(binaryData);\n\n// Process all XML files in the docx\nconst xmlFiles = Object.keys(zip.files).filter(f => f.endsWith('.xml'));\n\nfor (const xmlFile of xmlFiles) {\n let content = await zip.file(xmlFile).async('string');\n for (const [placeholder, value] of Object.entries(placeholders)) {\n // Handle split placeholders in XML (Word splits text into runs)\n // Simple approach: replace in the raw XML\n const escaped = placeholder.replace(/[{}]/g, c => `\\\\${c}`);\n content = content.split(placeholder).join(value);\n }\n zip.file(xmlFile, content);\n}\n\nconst filledDocx = await zip.generateAsync({ type: 'nodebuffer' });\n\nreturn [{\n json: { filename: `Mietvertrag_${orderData.order_number}.docx` },\n binary: {\n data: await this.helpers.prepareBinaryData(filledDocx, `Mietvertrag_${orderData.order_number}.docx`, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')\n }\n}];"
|
||||
},
|
||||
"id": "fill-template",
|
||||
"name": "Fill DOCX Template",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1330, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://gotenberg:3000/forms/libreoffice/convert",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"contentType": "multipart-form-data",
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"parameterType": "formBinaryData",
|
||||
"name": "files",
|
||||
"inputDataFieldName": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "convert-to-pdf",
|
||||
"name": "Convert to PDF (Gotenberg)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1550, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Rename the binary output to have the correct PDF filename\nconst orderData = $('Fetch Full Order+Customer').first().json;\nconst binaryData = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nreturn [{\n json: { filename: `Mietvertrag_${orderData.order_number}.pdf`, email: orderData.email, name: orderData.name, order_number: orderData.order_number },\n binary: {\n data: await this.helpers.prepareBinaryData(binaryData, `Mietvertrag_${orderData.order_number}.pdf`, 'application/pdf')\n }\n}];"
|
||||
},
|
||||
"id": "prepare-pdf",
|
||||
"name": "Prepare PDF Attachment",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1770, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fromEmail": "info@mc-cars.at",
|
||||
"toEmail": "={{ $json.email }}",
|
||||
"subject": "MC Cars – Ihr Mietvertrag {{ $json.order_number }}",
|
||||
"emailType": "html",
|
||||
"html": "<div style=\"font-family:Arial,sans-serif;max-width:600px;margin:0 auto;\">\n <div style=\"background:#1a1a1a;padding:20px;text-align:center;\">\n <h1 style=\"color:#c87941;margin:0;\">MC Cars</h1>\n </div>\n <div style=\"padding:30px;background:#f9f9f9;\">\n <p>Sehr geehrte/r <strong>{{ $json.name }}</strong>,</p>\n <p>anbei finden Sie Ihren Mietvertrag für die Bestellung <strong>{{ $json.order_number }}</strong>.</p>\n <p>Bitte prüfen Sie die Angaben und bringen Sie den unterschriebenen Vertrag zur Fahrzeugübergabe mit.</p>\n <hr style=\"border:none;border-top:1px solid #ddd;margin:25px 0;\" />\n <p>Bei Fragen stehen wir Ihnen jederzeit zur Verfügung.</p>\n <p>Mit freundlichen Grüßen,<br/><strong>MC Cars GmbH</strong><br/>info@mc-cars.at<br/>mc-cars.at</p>\n </div>\n</div>",
|
||||
"options": {
|
||||
"attachments": "data"
|
||||
}
|
||||
},
|
||||
"id": "send-mietvertrag",
|
||||
"name": "Send Mietvertrag Email",
|
||||
"type": "n8n-nodes-base.emailSend",
|
||||
"typeVersion": 2.1,
|
||||
"position": [1990, 200],
|
||||
"credentials": {
|
||||
"smtp": {
|
||||
"id": "2",
|
||||
"name": "MC Cars SMTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Postgres Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check Template Exists",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check Template Exists": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Template Exists?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Template Exists?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Full Order+Customer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
"Fetch Full Order+Customer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Download DOCX Template",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Download DOCX Template": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fill DOCX Template",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Fill DOCX Template": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Convert to PDF (Gotenberg)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Convert to PDF (Gotenberg)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Prepare PDF Attachment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Prepare PDF Attachment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Mietvertrag Email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [
|
||||
{
|
||||
"name": "mc-cars"
|
||||
}
|
||||
],
|
||||
"triggerCount": 1
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# n8n Workflows – MC Cars
|
||||
|
||||
This folder contains exportable n8n workflow definitions for the MC Cars qualification automation.
|
||||
|
||||
## Workflows
|
||||
|
||||
### 01 – Qualification Payment Email
|
||||
**Trigger:** Postgres `NOTIFY` on channel `lead_qualified` (fires when `qualify_lead()` creates a sales order).
|
||||
|
||||
**Flow:**
|
||||
1. Receives notification with `sales_order_id`, `customer_id`, etc.
|
||||
2. Fetches full order + customer data from Postgres.
|
||||
3. Sends HTML email to customer with:
|
||||
- Booking summary (vehicle, dates, pricing)
|
||||
- Kaution bank transfer instructions
|
||||
- Payment link for the rental amount
|
||||
|
||||
### 02 – Mietvertrag PDF Email
|
||||
**Trigger:** Same `lead_qualified` Postgres notification.
|
||||
|
||||
**Flow:**
|
||||
1. Checks if `mietvertrag_template_path` is set in `site_settings`.
|
||||
2. If no template → workflow stops (no error).
|
||||
3. If template exists:
|
||||
- Fetches customer + sales order data
|
||||
- Downloads DOCX template from `document-templates` storage bucket
|
||||
- Fills placeholders using JSZip (in a Code node)
|
||||
- Converts filled DOCX to PDF via Gotenberg
|
||||
- Sends PDF as email attachment to customer
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create Postgres credential in n8n
|
||||
- **Name:** `MC Cars Postgres`
|
||||
- **Host:** `db`
|
||||
- **Port:** `5432`
|
||||
- **Database:** `postgres`
|
||||
- **User:** `postgres`
|
||||
- **Password:** (value of `POSTGRES_PASSWORD` from `.env`)
|
||||
|
||||
### 2. Create SMTP credential in n8n
|
||||
- **Name:** `MC Cars SMTP`
|
||||
- **Host:** your SMTP server (e.g. `smtp.mailgun.org`, `mail.mc-cars.at`)
|
||||
- **Port:** `587` (TLS) or `465` (SSL)
|
||||
- **User:** your SMTP username
|
||||
- **Password:** your SMTP password
|
||||
- **From:** `info@mc-cars.at`
|
||||
|
||||
### 3. Import workflows
|
||||
1. Open n8n at http://localhost:55590
|
||||
2. Go to **Workflows** → **Import from file**
|
||||
3. Import `01-qualification-payment-email.json`
|
||||
4. Import `02-mietvertrag-pdf-email.json`
|
||||
5. Open each workflow → assign the credentials created above → **Activate**
|
||||
|
||||
### 4. Upload Mietvertrag template (optional)
|
||||
1. Open Admin panel → **Einstellungen** tab
|
||||
2. Upload a DOCX file in the "Mietvertrag-Vorlage" section
|
||||
3. The template should contain these placeholders:
|
||||
|
||||
| Placeholder | Replaced with |
|
||||
|---|---|
|
||||
| `{{KUNDE_NAME}}` | Customer full name |
|
||||
| `{{KUNDE_EMAIL}}` | Customer email |
|
||||
| `{{KUNDE_TELEFON}}` | Customer phone |
|
||||
| `{{BESTELLNUMMER}}` | Sales order number (e.g. SO-2026-0001) |
|
||||
| `{{FAHRZEUG}}` | Vehicle label (e.g. "Ferrari 488 GTB") |
|
||||
| `{{DATUM_VON}}` | Rental start date (DD.MM.YYYY) |
|
||||
| `{{DATUM_BIS}}` | Rental end date (DD.MM.YYYY) |
|
||||
| `{{TAGE_GESAMT}}` | Total rental days |
|
||||
| `{{WOCHENTAGE}}` | Number of weekdays |
|
||||
| `{{WOCHENENDTAGE}}` | Number of weekend days |
|
||||
| `{{TAGESSATZ}}` | Weekday daily subtotal |
|
||||
| `{{WOCHENENDZUSCHLAG}}` | Weekend surcharge subtotal |
|
||||
| `{{NETTO}}` | Net amount (excl. VAT) |
|
||||
| `{{MWST}}` | VAT amount (19%) |
|
||||
| `{{GESAMT}}` | Total amount (incl. VAT) |
|
||||
| `{{KAUTION}}` | Deposit amount |
|
||||
| `{{DATUM_HEUTE}}` | Today's date (DD.MM.YYYY) |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Gotenberg** (docker service `gotenberg`) — converts DOCX → PDF via LibreOffice
|
||||
- **n8n** needs the `jszip` npm package (pre-installed in n8n Docker image)
|
||||
|
||||
## Domain
|
||||
|
||||
All email links and sender addresses use `mc-cars.at`.
|
||||
@@ -0,0 +1,66 @@
|
||||
-- 10-mietvertrag-workflow.sql
|
||||
-- Document templates bucket, mietvertrag setting, and qualification webhook trigger.
|
||||
|
||||
-- 1. Storage bucket for admin-uploaded document templates (private, admin-only)
|
||||
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
values (
|
||||
'document-templates',
|
||||
'document-templates',
|
||||
false,
|
||||
20971520,
|
||||
array['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
|
||||
)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- RLS: only authenticated (admin) can read/write document-templates
|
||||
drop policy if exists "templates_admin_select" on storage.objects;
|
||||
create policy "templates_admin_select"
|
||||
on storage.objects for select to authenticated
|
||||
using (bucket_id = 'document-templates');
|
||||
|
||||
drop policy if exists "templates_admin_insert" on storage.objects;
|
||||
create policy "templates_admin_insert"
|
||||
on storage.objects for insert to authenticated
|
||||
with check (bucket_id = 'document-templates');
|
||||
|
||||
drop policy if exists "templates_admin_update" on storage.objects;
|
||||
create policy "templates_admin_update"
|
||||
on storage.objects for update to authenticated
|
||||
using (bucket_id = 'document-templates');
|
||||
|
||||
-- 2. Site setting for Mietvertrag template (empty on startup)
|
||||
insert into public.site_settings (key, value)
|
||||
values ('mietvertrag_template_path', '')
|
||||
on conflict (key) do nothing;
|
||||
|
||||
-- 3. Function to notify n8n when a lead is qualified (new sales order created)
|
||||
-- n8n listens on the 'lead_qualified' channel via Postgres trigger.
|
||||
create or replace function public.notify_lead_qualified()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
perform pg_notify('lead_qualified', json_build_object(
|
||||
'sales_order_id', NEW.id,
|
||||
'customer_id', NEW.customer_id,
|
||||
'lead_id', NEW.lead_id,
|
||||
'order_number', NEW.order_number,
|
||||
'total_eur', NEW.total_eur,
|
||||
'deposit_eur', NEW.deposit_eur,
|
||||
'date_from', NEW.date_from,
|
||||
'date_to', NEW.date_to,
|
||||
'vehicle_label', NEW.vehicle_label
|
||||
)::text);
|
||||
return NEW;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Trigger on sales_orders insert (fires when qualify_lead creates the order)
|
||||
drop trigger if exists trg_notify_lead_qualified on public.sales_orders;
|
||||
create trigger trg_notify_lead_qualified
|
||||
after insert on public.sales_orders
|
||||
for each row
|
||||
execute function public.notify_lead_qualified();
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user