Files
smb-online/n8n/onboarding.json
T
mivanchenko aabd1d56c4
Test backoffice (smb-crm) / test (push) Has been cancelled
New-client onboarding: provision resources/services/owner login, drop EA (#24)
n8n/onboarding.json now provisions a default resource (with Mon-Sat 09:00-18:00
hours so the public booking page has slots immediately), a starter service, and
an owner-login user for every new client, recording the temp password via the
existing credentials CRM entity -- gated behind an If check so a failed user
creation can't leave a stale credentials row. The EA-provisioning chain
(service/provider creation against Easy!Appointments) is removed entirely.

Adds POST /api/resources, /api/services, /api/owner_users to the backoffice API
for n8n to call, backed by booking_db.py's existing tenancy-safe create_*
helpers. Also adds "slug" to db.py's clients column list -- it was already a DB
column (#17) but the generic /api/clients POST silently dropped it, so
onboarding could never actually set a client's public-facing slug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:11:38 +02:00

416 lines
12 KiB
JSON

{
"name": "SMB · Onboarding",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "onboard",
"responseMode": "onReceived",
"options": {
"allowedOrigins": "*"
}
},
"id": "d1a08bba-5cc5-48fa-8063-bda2d58cab15",
"name": "Onboard webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
],
"webhookId": "96c28466-ee4b-4aa5-8f06-52d098b497a7"
},
{
"parameters": {
"jsCode": "const w=$('Onboard webhook').first().json; const body=w.body||w;\nconst s=v=>{v=(v==null?'':String(v));return /^[=+\\-@\\t\\r]/.test(v)?\"'\"+v:v;};\nconst now=new Date();\nfunction pd(v){if(!v)return null;const m=String(v).match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if(m)return new Date(Date.UTC(+m[1],+m[2]-1,+m[3]));const d=new Date(v);return isNaN(d)?null:d;}\nfunction slugify(v){return String(v||'').toLowerCase().normalize('NFKD').replace(/[\\u0300-\\u036f]/g,'')\n .replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'')||'client';}\nconst cycle=String(body.billing_cycle||'monthly').toLowerCase();\nconst start=pd(body.start_date)||new Date(Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),now.getUTCDate()));\nconst startStr=start.toISOString().slice(0,10);\nconst tier=String(body.tier||'A').toUpperCase().includes('B')?'B':'A';\nconst services=String(body.services||'site, booking, leads');\nconst slug=slugify(body.business_name)+'-'+Math.random().toString(36).slice(2,6);\nconst client={business_name:s(body.business_name),owner_name:s(body.owner_name),\n email:s(body.email),phone:s(body.phone),niche:s(body.niche),tier,status:'onboarding',\n domain:s(body.domain),stack_notes:s(body.stack_notes),vault_ref:s(body.vault_ref),\n services:s(services),billing_cycle:cycle,monthly_fee_eur:(body.monthly_fee_eur||''),\n start_date:startStr,notes:s(body.notes),slug};\nconst checklist=services.split(',').map(x=>x.trim()).filter(Boolean).map(x=>x+'\\u2610').join(' ');\nconst gl=new Date(start);gl.setUTCDate(gl.getUTCDate()+14);const goLive=gl.toISOString().slice(0,10);\nconst project={project_id:'P-'+Date.now(),deliverable:'Onboarding & Setup',tier,checklist,\n go_live_date:goLive,status:'todo'};\nreturn [{json:{client,project,tier,services,cycle,monthly_fee_eur:(body.monthly_fee_eur||''),\n business_name:client.business_name}}];"
},
"id": "fe9a0f5d-c6f8-4328-a98e-edc022582ec3",
"name": "Compute",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"method": "POST",
"url": "http://smb-crm:8080/api/clients",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.client) }}",
"options": {}
},
"id": "22222222-3333-4444-5555-666666666666",
"name": "Save client",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
680,
300
],
"retryOnFail": true,
"maxTries": 4,
"waitBetweenTries": 3000
},
{
"parameters": {
"jsCode": "const c=$('Compute').first().json;\nconst cid=$('Save client').first().json.added;\nconst p=Object.assign({}, c.project, {client_id:cid});\nreturn [{json:p}];"
},
"id": "33333333-4444-5555-6666-777777777777",
"name": "Build project",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"method": "POST",
"url": "http://smb-crm:8080/api/projects",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"id": "44444444-5555-6666-7777-888888888888",
"name": "Save project",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1120,
300
],
"retryOnFail": true,
"maxTries": 4,
"waitBetweenTries": 3000
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "5499280257",
"text": "=✅ Neuer Kunde angelegt: {{ $('Save client').item.json.added }}\n{{ $('Compute').item.json.business_name }} — Tier {{ $('Compute').item.json.tier }}\nServices: {{ $('Compute').item.json.services }}\nGebühr: {{ $('Compute').item.json.monthly_fee_eur || '—' }}€/{{ $('Compute').item.json.cycle }}\nGo-Live Ziel: {{ $('Build project').item.json.go_live_date }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "ae19e543-1a6b-4466-8af8-bb436f51091b",
"name": "Notify Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1340,
300
],
"credentials": {
"telegramApi": {
"id": "d9s4Ec5ocEH8FAxh",
"name": "SMB Telegram (leads bot)"
}
}
},
{
"parameters": {
"jsCode": "const cid=$('Save client').first().json.added;\nconst c=($('Compute').first().json.client)||{};\nconst password=Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,6);\nconst hours=[0,1,2,3,4,5].map(weekday=>({weekday,opens_at:'09:00',closes_at:'18:00'}));\nconst resourceBody={client_id:cid,name:'Hauptressource',hours};\nconst serviceBody={client_id:cid,name:'Termin',duration_minutes:30,price:0};\nreturn [{json:{cid,resourceBody,serviceBody,userEmail:c.email,userPassword:password}}];"
},
"id": "prov-build-provisioning",
"name": "Build provisioning",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
520
]
},
{
"parameters": {
"options": {},
"method": "POST",
"url": "http://smb-crm:8080/api/resources",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.resourceBody) }}"
},
"id": "prov-create-resource",
"name": "Create resource",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
420
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"continueOnFail": true
},
{
"parameters": {
"options": {},
"method": "POST",
"url": "http://smb-crm:8080/api/services",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.serviceBody) }}"
},
"id": "prov-create-service",
"name": "Create service",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
520
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"continueOnFail": true
},
{
"parameters": {
"options": {},
"method": "POST",
"url": "http://smb-crm:8080/api/owner_users",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ client_id: $json.cid, email: $json.userEmail, password: $json.userPassword }) }}"
},
"id": "prov-create-owner-user",
"name": "Create owner user",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
620
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"continueOnFail": true
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c0000000-1111-2222-3333-aaaaaaaaaaaa",
"leftValue": "={{ $json.user_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "prov-user-created-check",
"name": "User created?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1120,
620
]
},
{
"parameters": {
"options": {},
"method": "POST",
"url": "http://smb-crm:8080/api/credentials",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-CRM-Token",
"value": "__CRM_TOKEN__"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ client_id: $('Build provisioning').item.json.cid, label: 'Owner-Login', username: $('Build provisioning').item.json.userEmail, secret: $('Build provisioning').item.json.userPassword }) }}"
},
"id": "prov-save-credential",
"name": "Save credential",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
620
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"continueOnFail": true
}
],
"connections": {
"Onboard webhook": {
"main": [
[
{
"node": "Compute",
"type": "main",
"index": 0
}
]
]
},
"Compute": {
"main": [
[
{
"node": "Save client",
"type": "main",
"index": 0
}
]
]
},
"Save client": {
"main": [
[
{
"node": "Build project",
"type": "main",
"index": 0
},
{
"node": "Build provisioning",
"type": "main",
"index": 0
}
]
]
},
"Build project": {
"main": [
[
{
"node": "Save project",
"type": "main",
"index": 0
}
]
]
},
"Save project": {
"main": [
[
{
"node": "Notify Telegram",
"type": "main",
"index": 0
}
]
]
},
"Build provisioning": {
"main": [
[
{
"node": "Create resource",
"type": "main",
"index": 0
},
{
"node": "Create service",
"type": "main",
"index": 0
},
{
"node": "Create owner user",
"type": "main",
"index": 0
}
]
]
},
"Create owner user": {
"main": [
[
{
"node": "User created?",
"type": "main",
"index": 0
}
]
]
},
"User created?": {
"main": [
[
{
"node": "Save credential",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
}
}