[
  {
    "updatedAt": "2026-10-27T05:52:26.000Z",
    "createdAt": "2026-10-27T05:52:25.444Z",
    "id": "BookingCrmInvoice01",
    "name": "Booking form → CRM → invoice → Slack (sample, placeholder data)",
    "description": null,
    "active": true,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "booking-form",
          "responseMode": "responseNode",
          "options": {}
        },
        "id": "ccedf843-c1a9-5346-8574-8fe7924524d0",
        "name": "Booking form",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          0,
          300
        ],
        "webhookId": "0ebac3ac-1399-55d7-9f57-d2800bd89998"
      },
      {
        "parameters": {
          "mode": "runOnceForEachItem",
          "jsCode": "// Validate and normalise one booking-form submission, then price it from the rate card.\n// Runs once per item. Output: { valid, errors[], customer{}, job{}, lines[], subtotal }\nconst b = $json.body || $json;\nconst RATES = {\n  standard:     { label: 'Standard clean',     base: 139, bed: 25, bath: 30 },\n  deep:         { label: 'Deep clean',         base: 239, bed: 40, bath: 45 },\n  end_of_lease: { label: 'End-of-lease clean', base: 349, bed: 60, bath: 55 },\n};\nconst EXTRAS = {\n  oven:         { label: 'Oven clean',                 price: 69 },\n  fridge:       { label: 'Fridge clean',               price: 39 },\n  balcony:      { label: 'Balcony or courtyard',       price: 45 },\n  carpet_steam: { label: 'Carpet steam (per bedroom)', price: 35, per: 'bedroom' },\n  windows:      { label: 'Interior windows (per pane)', price: 8, per: 'pane' },\n};\nconst errors = [];\nconst email = String(b.email || '').trim().toLowerCase();\nif (!/^[^@\\s]+@[^@\\s]+\\.[a-z]{2,}$/.test(email)) errors.push('A valid email address is required');\nconst titleCase = s => String(s || '').trim().toLowerCase().replace(/\\b[a-z]/g, c => c.toUpperCase());\nconst name = titleCase(b.name);\nif (!name) errors.push('Name is required');\nconst rate = RATES[b.service];\nif (!rate) errors.push(`Service \"${b.service}\" is not on the rate card`);\nconst beds = Number(b.bedrooms), baths = Number(b.bathrooms);\nif (!(beds >= 1 && beds <= 6) || !(baths >= 1 && baths <= 4)) errors.push('Bedrooms 1-6 and bathrooms 1-4');\nconst digits = String(b.phone || '').replace(/\\D/g, '');\nconst phone = digits.length === 10 ? `${digits.slice(0, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}` : String(b.phone || '');\nconst lines = [];\nif (rate) {\n  lines.push({ code: b.service, description: `${rate.label}, ${beds} bed / ${baths} bath`, qty: 1,\n               unit: rate.base + rate.bed * (beds - 1) + rate.bath * (baths - 1) });\n  for (const x of (b.extras || [])) {\n    const e = EXTRAS[x];\n    if (!e) { errors.push(`Unknown extra \"${x}\"`); continue; }\n    const qty = e.per === 'bedroom' ? beds : e.per === 'pane' ? Number(b.window_panes || 0) : 1;\n    if (qty > 0) lines.push({ code: x, description: e.label, qty, unit: e.price });\n  }\n}\nfor (const l of lines) l.amount = Math.round(l.qty * l.unit * 100) / 100;\nconst subtotal = Math.round(lines.reduce((s, l) => s + l.amount, 0) * 100) / 100;\nreturn {\n  valid: errors.length === 0, errors, form_id: b.form_id, submitted_at: b.submitted_at,\n  customer: { name, email, phone, suburb: titleCase(b.suburb) },\n  job: { service: b.service, service_label: rate ? rate.label : b.service, bedrooms: beds, bathrooms: baths,\n         preferred_date: b.preferred_date, notes: String(b.notes || '').trim() },\n  lines, subtotal,\n};\n"
        },
        "id": "94e593a1-d631-513d-965c-4933aff97c8d",
        "name": "Validate & price",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          220,
          300
        ]
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "leftValue": "",
              "typeValidation": "strict"
            },
            "conditions": [
              {
                "id": "valid-check",
                "leftValue": "={{ $json.valid }}",
                "rightValue": true,
                "operator": {
                  "type": "boolean",
                  "operation": "true",
                  "singleValue": true
                }
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "id": "e5a16d13-974b-589d-b10c-9e7795093045",
        "name": "Valid?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2,
        "position": [
          440,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "http://127.0.0.1:18765/crm/contacts/upsert",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json.customer) }}",
          "options": {}
        },
        "id": "cad41361-0258-586f-b656-dd08a1ea26e5",
        "name": "CRM: upsert contact",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          660,
          200
        ],
        "notes": "Matches on email. Creates the contact if new, otherwise updates name, phone and suburb."
      },
      {
        "parameters": {
          "method": "POST",
          "url": "http://127.0.0.1:18765/crm/deals",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({contact_id: $json.contact.id, title: $('Validate & price').item.json.job.service_label + ' · ' + $('Validate & price').item.json.form_id, value: $('Validate & price').item.json.subtotal, stage: 'Booked', preferred_date: $('Validate & price').item.json.job.preferred_date}) }}",
          "options": {}
        },
        "id": "de25c864-dd16-50bb-8858-982bce491efb",
        "name": "CRM: create deal",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          880,
          200
        ],
        "notes": "One deal per booking, at the Booked stage."
      },
      {
        "parameters": {
          "mode": "runOnceForEachItem",
          "jsCode": "// Build the invoice from the validated booking and the CRM contact.\n// Returning-customer rule: 5% off when the CRM shows 3 or more previous jobs.\nconst v = $('Validate & price').item.json;\nconst contact = $('CRM: upsert contact').item.json.contact;\nconst deal = $json.deal;\nconst lines = v.lines.map(l => ({ ...l }));\nif (contact.jobs_before >= 3) {\n  lines.push({ code: 'loyalty', description: 'Returning-customer discount (5%)', qty: 1,\n               unit: -Math.round(v.subtotal * 5) / 100 });\n  lines[lines.length - 1].amount = lines[lines.length - 1].unit;\n}\nconst issue = v.submitted_at.slice(0, 10);\nconst due = new Date(issue + 'T00:00:00Z'); due.setUTCDate(due.getUTCDate() + 7);\nreturn {\n  contact_id: contact.id, deal_id: deal.id, form_id: v.form_id,\n  customer: v.customer, issue_date: issue, due_date: due.toISOString().slice(0, 10),\n  reference: `${v.job.service_label}, ${v.job.preferred_date}`, lines,\n  total: Math.round(lines.reduce((s, l) => s + l.amount, 0) * 100) / 100,\n};\n"
        },
        "id": "35a7ef86-6958-5598-8e3e-b931a9f470ff",
        "name": "Build invoice",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1100,
          200
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "http://127.0.0.1:18765/invoices",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {}
        },
        "id": "5539e230-d0ff-5f38-817b-fc53505f8ac9",
        "name": "Invoicing: create invoice",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1320,
          200
        ],
        "notes": "Creates the invoice as awaiting payment, due in 7 days. The invoicing app recomputes the total."
      },
      {
        "parameters": {
          "mode": "runOnceForEachItem",
          "jsCode": "// Format the #new-bookings Slack message (Block Kit) from the created invoice.\nconst v = $('Validate & price').item.json;\nconst c = $('CRM: upsert contact').item.json;\nconst inv = $json.invoice;\nconst money = n => '$' + n.toLocaleString('en-AU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst who = c.action === 'created' ? 'New customer' : `Returning customer (${c.contact.jobs_before} previous jobs)`;\nconst text = `New booking ${v.form_id}: ${v.customer.name}, ${v.job.service_label}, ${money(inv.total)}, invoice ${inv.number}`;\nreturn {\n  text,\n  blocks: [\n    { type: 'header', text: { type: 'plain_text', text: `New booking: ${v.job.service_label}` } },\n    { type: 'section', fields: [\n      { type: 'mrkdwn', text: `*Customer*\\n${v.customer.name} · ${v.customer.suburb}` },\n      { type: 'mrkdwn', text: `*CRM*\\n${who}` },\n      { type: 'mrkdwn', text: `*Preferred date*\\n${v.job.preferred_date}` },\n      { type: 'mrkdwn', text: `*Invoice*\\n${inv.number} · ${money(inv.total)} · due ${inv.due_date}` } ] },\n    ...(v.job.notes ? [{ type: 'context', elements: [{ type: 'mrkdwn', text: `Note: ${v.job.notes}` }] }] : []),\n  ],\n};\n"
        },
        "id": "346a697f-a1bd-5cb5-9d74-b981b7fba19d",
        "name": "Format Slack message",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1540,
          200
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "http://127.0.0.1:18765/slack/webhook",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {}
        },
        "id": "bc18f700-9075-5961-a367-5aa397f7c84c",
        "name": "Slack: #new-bookings",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1760,
          200
        ],
        "notes": "Incoming-webhook post to the team channel."
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ok: true, form_id: $('Validate & price').item.json.form_id, contact_id: $('CRM: upsert contact').item.json.contact.id, invoice: $('Invoicing: create invoice').item.json.invoice.number, total: $('Invoicing: create invoice').item.json.invoice.total}) }}",
          "options": {}
        },
        "id": "8b3200d2-51eb-590d-a693-a3aab01d0575",
        "name": "Respond: booked",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          1980,
          200
        ]
      },
      {
        "parameters": {
          "mode": "runOnceForEachItem",
          "jsCode": "// Invalid submission: nothing goes to the CRM or invoicing; the team gets a review alert instead.\nreturn {\n  text: `Booking form ${$json.form_id} needs a manual check: ${$json.errors.join('; ')}`,\n  blocks: [\n    { type: 'header', text: { type: 'plain_text', text: `Needs review: form ${$json.form_id}` } },\n    { type: 'section', text: { type: 'mrkdwn', text: `*${$json.customer.name || 'No name'}*\\n${$json.errors.map(e => '• ' + e).join('\\n')}` } },\n  ],\n};\n"
        },
        "id": "d3254ef6-9c46-5450-9989-3c50c9ed615e",
        "name": "Format review alert",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          660,
          440
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "http://127.0.0.1:18765/slack/webhook",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {}
        },
        "id": "5919fd5e-f850-5ea1-8931-7f16031942af",
        "name": "Slack: #bookings-review",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          880,
          440
        ],
        "notes": "Invalid forms never reach the CRM or invoicing."
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ok: false, form_id: $('Validate & price').item.json.form_id, errors: $('Validate & price').item.json.errors}) }}",
          "options": {
            "responseCode": 422
          }
        },
        "id": "736fd836-aeef-5e01-bf48-35ed8eb109ef",
        "name": "Respond: needs review",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          1100,
          440
        ]
      },
      {
        "parameters": {
          "content": "## Booking form → CRM → invoice → Slack (Sample project, placeholder data)\nForm webhook → validate & price from the rate card → upsert CRM contact + deal → create invoice (7-day terms, 5% returning-customer discount) → post to #new-bookings. Invalid forms go to #bookings-review. The CRM, invoicing and Slack endpoints here are **local mock services**; swap the URLs and add credentials to go live.",
          "height": 150,
          "width": 1180
        },
        "id": "083bf48e-20bc-58c0-a927-7b2d6dbf0946",
        "name": "Note",
        "type": "n8n-nodes-base.stickyNote",
        "typeVersion": 1,
        "position": [
          -20,
          40
        ]
      }
    ],
    "connections": {
      "Booking form": {
        "main": [
          [
            {
              "node": "Validate & price",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Validate & price": {
        "main": [
          [
            {
              "node": "Valid?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Valid?": {
        "main": [
          [
            {
              "node": "CRM: upsert contact",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Format review alert",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CRM: upsert contact": {
        "main": [
          [
            {
              "node": "CRM: create deal",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CRM: create deal": {
        "main": [
          [
            {
              "node": "Build invoice",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build invoice": {
        "main": [
          [
            {
              "node": "Invoicing: create invoice",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Invoicing: create invoice": {
        "main": [
          [
            {
              "node": "Format Slack message",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Format Slack message": {
        "main": [
          [
            {
              "node": "Slack: #new-bookings",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Slack: #new-bookings": {
        "main": [
          [
            {
              "node": "Respond: booked",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Format review alert": {
        "main": [
          [
            {
              "node": "Slack: #bookings-review",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Slack: #bookings-review": {
        "main": [
          [
            {
              "node": "Respond: needs review",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveDataSuccessExecution": "all",
      "saveManualExecutions": true
    },
    "staticData": null,
    "meta": null,
    "pinData": {},
    "versionId": "811236b0-dabc-4235-9f78-8d6b0dfa2530",
    "activeVersionId": "811236b0-dabc-4235-9f78-8d6b0dfa2530",
    "versionCounter": 3,
    "triggerCount": 1,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-27T05:52:25.446Z",
        "createdAt": "2026-10-27T05:52:25.446Z",
        "role": "workflow:owner",
        "workflowId": "BookingCrmInvoice01",
        "projectId": "daL8ObWmwWjwC04C",
        "project": {
          "updatedAt": "2026-10-27T05:52:25.043Z",
          "createdAt": "2026-10-27T05:52:25.043Z",
          "id": "daL8ObWmwWjwC04C",
          "name": "Unnamed Project",
          "type": "personal",
          "icon": null,
          "description": null
        }
      }
    ]
  }
]