New-client onboarding: provision resources/services/owner login, drop EA (#24)
Test backoffice (smb-crm) / test (push) Has been cancelled
Test backoffice (smb-crm) / test (push) Has been cancelled
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>
This commit is contained in:
+75
-1
@@ -7,7 +7,7 @@ machine-to-machine ingest path (n8n) is gated by the CRM_API_TOKEN header.
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, date, timezone
|
from datetime import datetime, date, time as dtime, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from flask import Flask, jsonify, request, Response
|
from flask import Flask, jsonify, request, Response
|
||||||
@@ -209,6 +209,80 @@ def delete_entity(entity, ident):
|
|||||||
return jsonify({"deleted": ident})
|
return jsonify({"deleted": ident})
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_hours_time(value):
|
||||||
|
try:
|
||||||
|
return dtime.fromisoformat(str(value))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/resources")
|
||||||
|
def create_resource_route():
|
||||||
|
"""Onboarding provisioning (#24): a default bookable resource for a new
|
||||||
|
client, with opening hours set inline so the public /book/<slug> page has
|
||||||
|
a slot grid to show immediately -- a resource without resource_hours has
|
||||||
|
no available slots (booking_api._available_slots)."""
|
||||||
|
if not authed():
|
||||||
|
return jsonify({"error": "forbidden"}), 403
|
||||||
|
body = request.get_json(force=True, silent=True) or {}
|
||||||
|
client_id = body.get("client_id")
|
||||||
|
name = (body.get("name") or "").strip()
|
||||||
|
if not client_id or not name:
|
||||||
|
return jsonify({"error": "client_id and name required"}), 400
|
||||||
|
row = bdb.create_resource(client_id, name)
|
||||||
|
for h in body.get("hours") or []:
|
||||||
|
opens_at = _parse_hours_time(h.get("opens_at"))
|
||||||
|
closes_at = _parse_hours_time(h.get("closes_at"))
|
||||||
|
if opens_at is None or closes_at is None:
|
||||||
|
continue
|
||||||
|
bdb.set_resource_hours(client_id, row["resource_id"], h.get("weekday"),
|
||||||
|
opens_at, closes_at)
|
||||||
|
with db.connect() as conn, conn.cursor() as cur:
|
||||||
|
log_activity(cur, client_id, "add resource", f"resource_id={row['resource_id']}")
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({"resource_id": row["resource_id"]}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/services")
|
||||||
|
def create_service_route():
|
||||||
|
"""Onboarding provisioning (#24): a starter service for a new client."""
|
||||||
|
if not authed():
|
||||||
|
return jsonify({"error": "forbidden"}), 403
|
||||||
|
body = request.get_json(force=True, silent=True) or {}
|
||||||
|
client_id = body.get("client_id")
|
||||||
|
name = (body.get("name") or "").strip()
|
||||||
|
duration_minutes = body.get("duration_minutes")
|
||||||
|
if not client_id or not name or not duration_minutes:
|
||||||
|
return jsonify({"error": "client_id, name and duration_minutes required"}), 400
|
||||||
|
row = bdb.create_service(client_id, name, duration_minutes, price=body.get("price"))
|
||||||
|
with db.connect() as conn, conn.cursor() as cur:
|
||||||
|
log_activity(cur, client_id, "add service", f"service_id={row['service_id']}")
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({"service_id": row["service_id"]}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/owner_users")
|
||||||
|
def create_owner_user_route():
|
||||||
|
"""Onboarding provisioning (#24): the owner's login for a new client, with
|
||||||
|
a temp password the caller (n8n) is expected to record via the
|
||||||
|
credentials CRM entity, same as it did for the retired EA login."""
|
||||||
|
if not authed():
|
||||||
|
return jsonify({"error": "forbidden"}), 403
|
||||||
|
body = request.get_json(force=True, silent=True) or {}
|
||||||
|
client_id = body.get("client_id")
|
||||||
|
email = (body.get("email") or "").strip().lower()
|
||||||
|
password = body.get("password") or ""
|
||||||
|
if not client_id or not email or not password:
|
||||||
|
return jsonify({"error": "client_id, email and password required"}), 400
|
||||||
|
if bdb.find_user_by_email(email) is not None:
|
||||||
|
return jsonify({"error": "email already in use"}), 409
|
||||||
|
row = bdb.create_user(client_id, email, password)
|
||||||
|
with db.connect() as conn, conn.cursor() as cur:
|
||||||
|
log_activity(cur, client_id, "add owner user", f"user_id={row['user_id']}")
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({"user_id": row["user_id"]}), 201
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/owner_users")
|
@app.get("/api/owner_users")
|
||||||
def list_owner_users():
|
def list_owner_users():
|
||||||
"""Owner-accounts list for the CRM dashboard's new tab (#19). Requires
|
"""Owner-accounts list for the CRM dashboard's new tab (#19). Requires
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ TABLES = {
|
|||||||
"cols": ["client_id", "business_name", "owner_name", "email", "phone",
|
"cols": ["client_id", "business_name", "owner_name", "email", "phone",
|
||||||
"niche", "tier", "status", "domain", "stack_notes", "vault_ref",
|
"niche", "tier", "status", "domain", "stack_notes", "vault_ref",
|
||||||
"services", "billing_cycle", "monthly_fee_eur", "start_date",
|
"services", "billing_cycle", "monthly_fee_eur", "start_date",
|
||||||
"renewal_date", "created_at", "notes", "notify_channel"],
|
"renewal_date", "created_at", "notes", "notify_channel", "slug"],
|
||||||
"dates": ["start_date", "renewal_date"],
|
"dates": ["start_date", "renewal_date"],
|
||||||
"timestamps": ["created_at"],
|
"timestamps": ["created_at"],
|
||||||
"numbers": ["monthly_fee_eur"],
|
"numbers": ["monthly_fee_eur"],
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Flask test client / real-DB integration tests for the onboarding
|
||||||
|
provisioning endpoints (#24): POST /api/resources, /api/services, and
|
||||||
|
/api/owner_users, used by n8n/onboarding.json in place of the retired EA
|
||||||
|
provider-account chain.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import app as app_module
|
||||||
|
import booking_db as bdb
|
||||||
|
from app import app as flask_app
|
||||||
|
|
||||||
|
CLIENT_A = "C-TEST-PROV-A"
|
||||||
|
CLIENT_B = "C-TEST-PROV-B"
|
||||||
|
AUTH = {"X-CRM-Token": "test-crm-token"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(monkeypatch):
|
||||||
|
flask_app.config["TESTING"] = True
|
||||||
|
monkeypatch.setattr(app_module, "CRM_TOKEN", "test-crm-token")
|
||||||
|
return flask_app.test_client()
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_client(client_id):
|
||||||
|
with bdb.db.connect() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO clients (client_id, business_name) VALUES (%s, %s) "
|
||||||
|
"ON CONFLICT (client_id) DO NOTHING",
|
||||||
|
(client_id, "Café " + client_id))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- /api/clients slug (generic entity POST, #24) ----
|
||||||
|
|
||||||
|
def test_create_client_persists_slug(client):
|
||||||
|
resp = client.post("/api/clients", headers=AUTH, json={
|
||||||
|
"business_name": "Slug Test Client", "slug": "slug-test-abcd"})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
client_id = resp.get_json()["added"]
|
||||||
|
assert bdb.get_client_by_slug("slug-test-abcd")["client_id"] == client_id
|
||||||
|
|
||||||
|
|
||||||
|
# ---- /api/resources ----
|
||||||
|
|
||||||
|
def test_create_resource_requires_crm_token(client):
|
||||||
|
resp = client.post("/api/resources", json={"client_id": CLIENT_A, "name": "Hauptressource"})
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_resource_sets_hours(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/resources", headers=AUTH, json={
|
||||||
|
"client_id": CLIENT_A, "name": "Hauptressource",
|
||||||
|
"hours": [{"weekday": 0, "opens_at": "09:00", "closes_at": "18:00"}]})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
resource_id = resp.get_json()["resource_id"]
|
||||||
|
hours = bdb.get_resource_hours(CLIENT_A, resource_id)
|
||||||
|
assert 0 in hours
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_resource_requires_name(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/resources", headers=AUTH, json={"client_id": CLIENT_A})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
# ---- /api/services ----
|
||||||
|
|
||||||
|
def test_create_service_requires_crm_token(client):
|
||||||
|
resp = client.post("/api/services", json={
|
||||||
|
"client_id": CLIENT_A, "name": "Termin", "duration_minutes": 30})
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_service_creates_active_service(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/services", headers=AUTH, json={
|
||||||
|
"client_id": CLIENT_A, "name": "Termin", "duration_minutes": 30})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
service_id = resp.get_json()["service_id"]
|
||||||
|
row = bdb.get_service(CLIENT_A, service_id)
|
||||||
|
assert row["active"] is True
|
||||||
|
assert row["duration_minutes"] == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_service_requires_duration(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/services", headers=AUTH,
|
||||||
|
json={"client_id": CLIENT_A, "name": "Termin"})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
# ---- /api/owner_users ----
|
||||||
|
|
||||||
|
def test_create_owner_user_requires_crm_token(client):
|
||||||
|
resp = client.post("/api/owner_users", json={
|
||||||
|
"client_id": CLIENT_A, "email": "owner@example.com", "password": "pw12345"})
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_owner_user_creates_login(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/owner_users", headers=AUTH, json={
|
||||||
|
"client_id": CLIENT_A, "email": "owner@example.com", "password": "pw12345"})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
user = bdb.find_user_by_email("owner@example.com")
|
||||||
|
assert user["client_id"] == CLIENT_A
|
||||||
|
assert bdb.verify_password(user, "pw12345")
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_owner_user_rejects_duplicate_email(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
_insert_client(CLIENT_B)
|
||||||
|
bdb.create_user(CLIENT_A, "owner@example.com", "pw12345")
|
||||||
|
resp = client.post("/api/owner_users", headers=AUTH, json={
|
||||||
|
"client_id": CLIENT_B, "email": "owner@example.com", "password": "pw67890"})
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_owner_user_requires_password(client):
|
||||||
|
_insert_client(CLIENT_A)
|
||||||
|
resp = client.post("/api/owner_users", headers=AUTH,
|
||||||
|
json={"client_id": CLIENT_A, "email": "owner@example.com"})
|
||||||
|
assert resp.status_code == 400
|
||||||
+101
-110
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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;}\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 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)};\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}}];"
|
"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",
|
"id": "fe9a0f5d-c6f8-4328-a98e-edc022582ec3",
|
||||||
"name": "Compute",
|
"name": "Compute",
|
||||||
@@ -133,10 +133,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const cid=$('Save client').first().json.added;\nconst biz=$('Compute').first().json.business_name||'Kunde';\nconst slug=String(cid).toLowerCase().replace(/[^a-z0-9]/g,'');\nconst password=Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,6);\nconst serviceBody={name:'Termin',duration:30,price:0,currency:'EUR',availabilitiesType:'flexible',attendantsNumber:1,isPrivate:false};\nreturn [{json:{cid,biz,username:slug,password,serviceBody}}];"
|
"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": "ea-ea-build-service",
|
"id": "prov-build-provisioning",
|
||||||
"name": "EA build service",
|
"name": "Build provisioning",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
@@ -148,13 +148,44 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"options": {},
|
"options": {},
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"url": "http://easyappointments/index.php/api/v1/services",
|
"url": "http://smb-crm:8080/api/resources",
|
||||||
"sendHeaders": true,
|
"sendHeaders": true,
|
||||||
"headerParameters": {
|
"headerParameters": {
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "X-CRM-Token",
|
||||||
"value": "Basic __EA_AUTH__"
|
"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__"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -162,8 +193,8 @@
|
|||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "={{ JSON.stringify($json.serviceBody) }}"
|
"jsonBody": "={{ JSON.stringify($json.serviceBody) }}"
|
||||||
},
|
},
|
||||||
"id": "ea-ea-create-service",
|
"id": "prov-create-service",
|
||||||
"name": "EA create service",
|
"name": "Create service",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.2,
|
"typeVersion": 4.2,
|
||||||
"position": [
|
"position": [
|
||||||
@@ -175,68 +206,11 @@
|
|||||||
"waitBetweenTries": 2000,
|
"waitBetweenTries": 2000,
|
||||||
"continueOnFail": true
|
"continueOnFail": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"jsCode": "const prep=$('EA build service').first().json;\nconst svcId=$json.id;\nconst c=($('Compute').first().json.client)||{};\nconst parts=String(prep.biz).trim().split(/\\s+/);\nconst firstName=parts[0]||prep.biz;\nconst lastName=parts.slice(1).join(' ')||'Studio';\nconst wp={start:'09:00',end:'18:00',breaks:[]};\nconst providerBody={firstName,lastName,email:'provider+'+prep.username+'@booking.mivanchenko.de',\n phone:c.phone||'',services:[svcId],isPrivate:false,timezone:'Europe/Berlin',notes:prep.cid,\n settings:{username:prep.username,password:prep.password,\n workingPlan:{monday:wp,tuesday:wp,wednesday:wp,thursday:wp,friday:wp,saturday:wp,sunday:null}}};\nreturn [{json:{providerBody,svcId,cid:prep.cid,username:prep.username,biz:prep.biz}}];"
|
|
||||||
},
|
|
||||||
"id": "ea-ea-build-provider",
|
|
||||||
"name": "EA build provider",
|
|
||||||
"type": "n8n-nodes-base.code",
|
|
||||||
"typeVersion": 2,
|
|
||||||
"position": [
|
|
||||||
1120,
|
|
||||||
520
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"options": {},
|
"options": {},
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"url": "http://easyappointments/index.php/api/v1/providers",
|
"url": "http://smb-crm:8080/api/owner_users",
|
||||||
"sendHeaders": true,
|
|
||||||
"headerParameters": {
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"value": "Basic __EA_AUTH__"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sendBody": true,
|
|
||||||
"specifyBody": "json",
|
|
||||||
"jsonBody": "={{ JSON.stringify($('EA build provider').item.json.providerBody) }}"
|
|
||||||
},
|
|
||||||
"id": "ea-ea-create-provider",
|
|
||||||
"name": "EA create provider",
|
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
|
||||||
"typeVersion": 4.2,
|
|
||||||
"position": [
|
|
||||||
1340,
|
|
||||||
520
|
|
||||||
],
|
|
||||||
"retryOnFail": true,
|
|
||||||
"maxTries": 3,
|
|
||||||
"waitBetweenTries": 2000,
|
|
||||||
"continueOnFail": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"jsCode": "const bp=$('EA build provider').first().json;\nconst provId=$json.id;\nconst embed='https://booking.mivanchenko.de/index.php/booking?service='+bp.svcId+'&provider='+provId;\nconst stack_notes='Buchung-Embed: '+embed+' | EA provider '+provId+' / svc '+bp.svcId+' / login '+bp.username;\nconst password=bp.providerBody.settings.password;\nreturn [{json:{cid:bp.cid,embed,provId,username:bp.username,password,patch:{stack_notes}}}];"
|
|
||||||
},
|
|
||||||
"id": "ea-ea-booking-info",
|
|
||||||
"name": "EA booking info",
|
|
||||||
"type": "n8n-nodes-base.code",
|
|
||||||
"typeVersion": 2,
|
|
||||||
"position": [
|
|
||||||
1560,
|
|
||||||
520
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"options": {},
|
|
||||||
"method": "PATCH",
|
|
||||||
"url": "=http://smb-crm:8080/api/clients/{{ $json.cid }}",
|
|
||||||
"sendHeaders": true,
|
"sendHeaders": true,
|
||||||
"headerParameters": {
|
"headerParameters": {
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -248,21 +222,54 @@
|
|||||||
},
|
},
|
||||||
"sendBody": true,
|
"sendBody": true,
|
||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "={{ JSON.stringify($json.patch) }}"
|
"jsonBody": "={{ JSON.stringify({ client_id: $json.cid, email: $json.userEmail, password: $json.userPassword }) }}"
|
||||||
},
|
},
|
||||||
"id": "ea-ea-update-client",
|
"id": "prov-create-owner-user",
|
||||||
"name": "EA update client",
|
"name": "Create owner user",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.2,
|
"typeVersion": 4.2,
|
||||||
"position": [
|
"position": [
|
||||||
1780,
|
900,
|
||||||
520
|
620
|
||||||
],
|
],
|
||||||
"retryOnFail": true,
|
"retryOnFail": true,
|
||||||
"maxTries": 3,
|
"maxTries": 3,
|
||||||
"waitBetweenTries": 2000,
|
"waitBetweenTries": 2000,
|
||||||
"continueOnFail": true
|
"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": {
|
"parameters": {
|
||||||
"options": {},
|
"options": {},
|
||||||
@@ -279,15 +286,15 @@
|
|||||||
},
|
},
|
||||||
"sendBody": true,
|
"sendBody": true,
|
||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "={{ JSON.stringify({ client_id: $json.cid, label: 'Easy!Appointments Provider-Login', username: $json.username, secret: $json.password }) }}"
|
"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": "ea-save-credential",
|
"id": "prov-save-credential",
|
||||||
"name": "Save credential",
|
"name": "Save credential",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.2,
|
"typeVersion": 4.2,
|
||||||
"position": [
|
"position": [
|
||||||
1780,
|
1340,
|
||||||
680
|
620
|
||||||
],
|
],
|
||||||
"retryOnFail": true,
|
"retryOnFail": true,
|
||||||
"maxTries": 3,
|
"maxTries": 3,
|
||||||
@@ -327,7 +334,7 @@
|
|||||||
"index": 0
|
"index": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"node": "EA build service",
|
"node": "Build provisioning",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
@@ -356,64 +363,48 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"EA build service": {
|
"Build provisioning": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "EA create service",
|
"node": "Create resource",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Create service",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Create owner user",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"EA create service": {
|
"Create owner user": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "EA build provider",
|
"node": "User created?",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"EA build provider": {
|
"User created?": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"node": "EA create provider",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"EA create provider": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "EA booking info",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"EA booking info": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "EA update client",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"node": "Save credential",
|
"node": "Save credential",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
[]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user