{
  "name": "Text-to-Book - 3CX to Mews",
  "nodes": [
    {
      "parameters": {
        "content": "A guest texts one of your property lines, this reads the enquiry and writes the reservation straight into Mews, so nobody keys it in by hand while the team is travelling.\n\nMVP, not connected yet",
        "height": 130,
        "width": 400
      },
      "name": "What this does",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [180, 40]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "3cx-text-to-book",
        "responseMode": "lastNode",
        "options": {}
      },
      "name": "Guest texts the hotel line",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 300],
      "webhookId": "3cx-text-to-book"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"gpt-4o-mini\",\n  \"temperature\": 0,\n  \"response_format\": { \"type\": \"json_object\" },\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You read guest enquiries texted to an Australian hotel group and return JSON only. Keys: guest_name, mobile, property (Mornington, Barossa, Noosa Heads, Kirribilli, or unknown), arrival (YYYY-MM-DD), departure (YYYY-MM-DD), adults, children, room_type (Queen, King, Twin, Family Suite, or unknown), special_requests. Use null when the guest did not say it. Never guess a date. Today is {{ $now.setZone('Australia/Melbourne').toFormat('yyyy-MM-dd') }} and the guest is writing in Australian time.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify($json.body.text || $json.body.message || '') }}\n    }\n  ]\n}",
        "options": {}
      },
      "name": "Read the enquiry (AI)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [280, 300]
    },
    {
      "parameters": {
        "jsCode": "// Turn the model's reply into the fields Mews wants, and decide if it is bookable.\nconst reply = $input.first().json.choices[0].message.content;\nconst g = JSON.parse(reply);\n\nconst inbound = $('Guest texts the hotel line').item.json.body || {};\n\n// 3CX hands us the caller's mobile, so trust that over anything typed in the text.\nconst mobile = (inbound.from || g.mobile || '').replace(/[^0-9+]/g, '');\n\nconst nights = (g.arrival && g.departure)\n  ? Math.round((new Date(g.departure) - new Date(g.arrival)) / 86400000)\n  : null;\n\nconst missing = [];\nif (!g.guest_name) missing.push('name');\nif (!g.property || g.property === 'unknown') missing.push('property');\nif (!g.arrival) missing.push('arrival date');\nif (!g.departure) missing.push('departure date');\nif (!nights || nights < 1) missing.push('a valid length of stay');\n\nreturn [{\n  json: {\n    guest_name: g.guest_name,\n    first_name: (g.guest_name || '').split(' ')[0] || null,\n    last_name: (g.guest_name || '').split(' ').slice(1).join(' ') || null,\n    mobile,\n    property: g.property,\n    arrival: g.arrival,\n    departure: g.departure,\n    nights,\n    adults: g.adults || 2,\n    children: g.children || 0,\n    room_type: g.room_type === 'unknown' ? null : g.room_type,\n    special_requests: g.special_requests,\n    missing,\n    ready_to_book: missing.length === 0,\n    raw_text: inbound.text || inbound.message || ''\n  }\n}];"
      },
      "name": "Tidy the booking details",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [560, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ready-to-book",
              "leftValue": "={{ $json.ready_to_book }}",
              "rightValue": "",
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Enough to book?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [840, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.mews.com/api/connector/v1/services/getAvailability",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"ClientToken\": \"MEWS_CLIENT_TOKEN\",\n  \"AccessToken\": \"MEWS_ACCESS_TOKEN\",\n  \"Client\": \"Text-to-Book 1.0\",\n  \"ServiceId\": \"SERVICE_ID_FOR_{{ ($json.property || '').toUpperCase().replace(' ', '_') }}\",\n  \"FirstTimeUnitStartUtc\": \"{{ $json.arrival }}T00:00:00Z\",\n  \"LastTimeUnitStartUtc\": \"{{ $json.departure }}T00:00:00Z\"\n}",
        "options": {}
      },
      "name": "Check Mews for free rooms",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1120, 180]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.mews.com/api/connector/v1/reservations/add",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"ClientToken\": \"MEWS_CLIENT_TOKEN\",\n  \"AccessToken\": \"MEWS_ACCESS_TOKEN\",\n  \"Client\": \"Text-to-Book 1.0\",\n  \"ServiceId\": \"SERVICE_ID_FOR_{{ ($('Tidy the booking details').item.json.property || '').toUpperCase().replace(' ', '_') }}\",\n  \"Reservations\": [\n    {\n      \"Identifier\": \"3cx-{{ $('Guest texts the hotel line').item.json.body.call_id || $now.toMillis() }}\",\n      \"State\": \"Optional\",\n      \"StartUtc\": \"{{ $('Tidy the booking details').item.json.arrival }}T14:00:00Z\",\n      \"EndUtc\": \"{{ $('Tidy the booking details').item.json.departure }}T10:00:00Z\",\n      \"AdultCount\": {{ $('Tidy the booking details').item.json.adults }},\n      \"ChildCount\": {{ $('Tidy the booking details').item.json.children }},\n      \"Notes\": {{ JSON.stringify(($('Tidy the booking details').item.json.special_requests || '') + ' | texted in: ' + $('Tidy the booking details').item.json.raw_text) }},\n      \"Customer\": {\n        \"FirstName\": {{ JSON.stringify($('Tidy the booking details').item.json.first_name) }},\n        \"LastName\": {{ JSON.stringify($('Tidy the booking details').item.json.last_name) }},\n        \"Phone\": {{ JSON.stringify($('Tidy the booking details').item.json.mobile) }}\n      }\n    }\n  ]\n}",
        "options": {}
      },
      "name": "Write the reservation into Mews",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1400, 180]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://PBX-HOSTNAME.my3cx.au/xapi/v1/Messages/SendSms",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"To\": {{ JSON.stringify($('Tidy the booking details').item.json.mobile) }},\n  \"Text\": {{ JSON.stringify('Thanks ' + $('Tidy the booking details').item.json.first_name + ', you are pencilled in at ' + $('Tidy the booking details').item.json.property + ' from ' + $('Tidy the booking details').item.json.arrival + ' to ' + $('Tidy the booking details').item.json.departure + ', ' + $('Tidy the booking details').item.json.nights + ' nights for ' + $('Tidy the booking details').item.json.adults + ' adults. Reply CHANGE if anything is wrong, otherwise reception will confirm shortly.') }}\n}",
        "options": {}
      },
      "name": "Text the guest their confirmation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1680, 180]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": { "__rl": true, "value": "#reservations", "mode": "name" },
        "text": "=Text enquiry I could not book on my own.\n\nFrom: {{ $json.mobile }}\nThey wrote: {{ $json.raw_text }}\nStill missing: {{ $json.missing.join(', ') }}\n\nWhat I did get: {{ $json.guest_name || 'no name' }} / {{ $json.property || 'no property' }} / {{ $json.arrival || 'no arrival' }} to {{ $json.departure || 'no departure' }}",
        "otherOptions": {}
      },
      "name": "Ask the duty manager",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [1120, 420]
    }
  ],
  "connections": {
    "Guest texts the hotel line": {
      "main": [[{ "node": "Read the enquiry (AI)", "type": "main", "index": 0 }]]
    },
    "Read the enquiry (AI)": {
      "main": [[{ "node": "Tidy the booking details", "type": "main", "index": 0 }]]
    },
    "Tidy the booking details": {
      "main": [[{ "node": "Enough to book?", "type": "main", "index": 0 }]]
    },
    "Enough to book?": {
      "main": [
        [{ "node": "Check Mews for free rooms", "type": "main", "index": 0 }],
        [{ "node": "Ask the duty manager", "type": "main", "index": 0 }]
      ]
    },
    "Check Mews for free rooms": {
      "main": [[{ "node": "Write the reservation into Mews", "type": "main", "index": 0 }]]
    },
    "Write the reservation into Mews": {
      "main": [[{ "node": "Text the guest their confirmation", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" }
}
