{
  "name": "Order calculator → Telegram staff bot → Google Sheets (demo)",
  "nodes": [
    {
      "parameters": {
        "content": "## Order calculator → Telegram → Google Sheets\n\nDemo workflow from igoshevmax.com/calculators.\n\n1. Telegram: create a bot with @BotFather, add a Telegram credential to **Send to staff chat**, **Staff button pressed**, **Answer button** and **Edit staff message**. Put your staff chat id into **Send to staff chat**.\n2. Google Sheets: add a credential to both sheet nodes, paste the sheet URL. Row 1 of the sheet: created_at, order_id, business, name, contact, date, items, total, note, status, staff, tg_message_id.\n3. Point the calculator at the production URL of **Order webhook** (POST, JSON).\n4. Activate the workflow.\n\nCredentials are intentionally empty.",
        "height": 420,
        "width": 520
      },
      "id": "c6f90598-00e1-49cd-8bbc-cc7ff37bb8b6",
      "name": "Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -80,
        -460
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "calc-order",
        "responseMode": "onReceived",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "id": "2721cc00-e400-4d3f-8507-bc8d826b0702",
      "name": "Order webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "3a5f330a-87c3-4c83-9041-4792f4942b47"
    },
    {
      "parameters": {
        "jsCode": "// Reads the order the website calculator posts, checks the total against the order's own\n// price lines, and writes the message for the staff chat. One item in, one item out.\nconst L = {\n  ru: { title: 'Новый заказ', total: 'Итог по прайсу', unit: 'ед.', name: 'Имя', contact: 'Контакт', date: 'Дата', note: 'Комментарий', status: 'Статус', fresh: 'новый', check: 'проверить итог', accept: 'Принять', ask: 'Уточнить', decline: 'Отклонить' },\n  en: { title: 'New order', total: 'Total by the price list', unit: 'units', name: 'Name', contact: 'Contact', date: 'Date', note: 'Notes', status: 'Status', fresh: 'new', check: 'check the total', accept: 'Accept', ask: 'Ask', decline: 'Decline' },\n};\nconst input = $input.first().json;\nconst body = input.body && typeof input.body === 'object' ? input.body : input;\nconst lang = body.lang === 'ru' ? 'ru' : 'en';\nconst t = L[lang];\nconst clip = (v, n = 200) => String(v ?? '').slice(0, n);\n\n// The browser computed the total; recount it from the lines it sent and flag a mismatch.\nconst lines = Array.isArray(body.lines) ? body.lines : [];\nconst mults = Array.isArray(body.multipliers) ? body.multipliers : [];\nconst sum = lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);\nconst recount = Math.round(mults.reduce((m, x) => m * (Number(x.value) || 1), sum));\nconst totalOk = Math.abs(recount - Number(body.total)) <= 1;\n\nconst c = body.customer || {};\nconst orderId = clip(body.order_id || 'ORD-' + String(Date.now()).slice(-6), 32);\nconst summary = (Array.isArray(body.summary) ? body.summary : []).map((s) => clip(s.label, 60) + ': ' + clip(s.value));\nconst text = [\n  t.title + ' ' + orderId,\n  clip(body.business, 80),\n  '',\n  ...summary,\n  '',\n  t.total + ': ' + recount + ' ' + t.unit,\n  '',\n  t.name + ': ' + clip(c.name, 60),\n  t.contact + ': ' + clip(c.contact, 60),\n  c.date ? t.date + ': ' + clip(c.date, 20) : null,\n  c.note ? t.note + ': ' + clip(c.note, 240) : null,\n  '',\n  t.status + ': ' + (totalOk ? t.fresh : t.check),\n].filter((x) => x !== null).join('\\n').slice(0, 4000);\n\nreturn [{\n  json: {\n    order_id: orderId,\n    created_at: clip(body.created_at, 40) || new Date().toISOString(),\n    business: clip(body.business, 80),\n    name: clip(c.name, 60),\n    contact: clip(c.contact, 60),\n    date: clip(c.date, 20),\n    note: clip(c.note, 240),\n    items: summary.join('; '),\n    total: recount,\n    status: totalOk ? 'new' : 'check_total',\n    labels: { accept: t.accept, ask: t.ask, decline: t.decline },\n    text,\n  },\n}];"
      },
      "id": "b7558513-abf7-44d6-947a-c6cc111c1ae1",
      "name": "Check total and build message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "chatId": "REPLACE_WITH_STAFF_CHAT_ID",
        "text": "={{ $json.text }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "={{ $json.labels.accept }}",
                    "additionalFields": {
                      "callback_data": "=accept:{{ $json.order_id }}"
                    }
                  },
                  {
                    "text": "={{ $json.labels.ask }}",
                    "additionalFields": {
                      "callback_data": "=ask:{{ $json.order_id }}"
                    }
                  },
                  {
                    "text": "={{ $json.labels.decline }}",
                    "additionalFields": {
                      "callback_data": "=decline:{{ $json.order_id }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "41404fe8-3f27-4c8b-a7bf-99daa76cdfcc",
      "name": "Send to staff chat",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "Orders",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "created_at": "={{ $('Check total and build message').item.json.created_at }}",
            "order_id": "={{ $('Check total and build message').item.json.order_id }}",
            "business": "={{ $('Check total and build message').item.json.business }}",
            "name": "={{ $('Check total and build message').item.json.name }}",
            "contact": "={{ $('Check total and build message').item.json.contact }}",
            "date": "={{ $('Check total and build message').item.json.date }}",
            "items": "={{ $('Check total and build message').item.json.items }}",
            "total": "={{ $('Check total and build message').item.json.total }}",
            "note": "={{ $('Check total and build message').item.json.note }}",
            "status": "={{ $('Check total and build message').item.json.status }}",
            "staff": "",
            "tg_message_id": "={{ $json.result.message_id }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "created_at",
              "displayName": "created_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "order_id",
              "displayName": "order_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "business",
              "displayName": "business",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "contact",
              "displayName": "contact",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "date",
              "displayName": "date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "items",
              "displayName": "items",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "total",
              "displayName": "total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "note",
              "displayName": "note",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "staff",
              "displayName": "staff",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tg_message_id",
              "displayName": "tg_message_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "df2e31fd-cc76-4366-ac0f-008b68f3be5a",
      "name": "Append order row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        720,
        0
      ]
    },
    {
      "parameters": {
        "updates": [
          "callback_query"
        ],
        "additionalFields": {}
      },
      "id": "4f1f1443-abcc-4aa3-90a6-12e086e5adbb",
      "name": "Staff button pressed",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "webhookId": "88c90ea8-0b13-459c-97f8-ea6c1c62db32"
    },
    {
      "parameters": {
        "jsCode": "// A staff member pressed a button under an order message. The button carries\n// \"<action>:<order_id>\"; the message text tells which language the order was in.\nconst q = $input.first().json.callback_query || {};\nconst [action, orderId] = String(q.data || '').split(':');\nconst original = (q.message && q.message.text) || '';\nconst ru = /[А-Яа-яЁё]/.test(original);\nconst st = ru\n  ? { accept: 'принят', ask: 'нужно уточнить', decline: 'отклонён', label: 'Статус', by: 'нажал' }\n  : { accept: 'accepted', ask: 'needs a question', decline: 'declined', label: 'Status', by: 'by' };\nconst labels = ru ? { accept: 'Принять', ask: 'Уточнить', decline: 'Отклонить' } : { accept: 'Accept', ask: 'Ask', decline: 'Decline' };\nconst from = q.from || {};\nconst staff = [from.first_name, from.last_name].filter(Boolean).join(' ') || from.username || String(from.id || '');\nconst status = st[action] || action;\nconst statusLine = new RegExp('^' + st.label + ':.*$', 'm');\nconst text = original.replace(statusLine, '').trimEnd() + '\\n\\n' + st.label + ': ' + status + ', ' + st.by + ' ' + staff;\n\nreturn [{\n  json: {\n    order_id: orderId,\n    status: action,\n    status_label: status,\n    staff,\n    query_id: q.id,\n    chat_id: q.message && q.message.chat ? q.message.chat.id : '',\n    message_id: q.message ? q.message.message_id : '',\n    text,\n    labels,\n  },\n}];"
      },
      "id": "97a3f3eb-2f78-456c-bd4d-7639973fafda",
      "name": "Parse button",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "Orders",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "order_id": "={{ $json.order_id }}",
            "status": "={{ $json.status }}",
            "staff": "={{ $json.staff }}"
          },
          "matchingColumns": [
            "order_id"
          ],
          "schema": [
            {
              "id": "created_at",
              "displayName": "created_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "order_id",
              "displayName": "order_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "business",
              "displayName": "business",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "contact",
              "displayName": "contact",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "date",
              "displayName": "date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "items",
              "displayName": "items",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "total",
              "displayName": "total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "note",
              "displayName": "note",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "staff",
              "displayName": "staff",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tg_message_id",
              "displayName": "tg_message_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "918aead2-6dd3-4db6-8801-286a7ac6f5e9",
      "name": "Update status in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "resource": "callback",
        "queryId": "={{ $('Parse button').item.json.query_id }}",
        "additionalFields": {
          "text": "={{ $('Parse button').item.json.status_label }}"
        }
      },
      "id": "06b6bcb3-9bb1-4bd1-acc2-17bb5ddc0808",
      "name": "Answer button",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "operation": "editMessageText",
        "chatId": "={{ $('Parse button').item.json.chat_id }}",
        "messageId": "={{ $('Parse button').item.json.message_id }}",
        "text": "={{ $('Parse button').item.json.text }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "={{ $('Parse button').item.json.labels.accept }}",
                    "additionalFields": {
                      "callback_data": "=accept:{{ $('Parse button').item.json.order_id }}"
                    }
                  },
                  {
                    "text": "={{ $('Parse button').item.json.labels.ask }}",
                    "additionalFields": {
                      "callback_data": "=ask:{{ $('Parse button').item.json.order_id }}"
                    }
                  },
                  {
                    "text": "={{ $('Parse button').item.json.labels.decline }}",
                    "additionalFields": {
                      "callback_data": "=decline:{{ $('Parse button').item.json.order_id }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {}
      },
      "id": "5539e788-62c6-45ca-954a-54c04fe42274",
      "name": "Edit staff message",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        960,
        300
      ]
    }
  ],
  "connections": {
    "Order webhook": {
      "main": [
        [
          {
            "node": "Check total and build message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check total and build message": {
      "main": [
        [
          {
            "node": "Send to staff chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to staff chat": {
      "main": [
        [
          {
            "node": "Append order row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Staff button pressed": {
      "main": [
        [
          {
            "node": "Parse button",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse button": {
      "main": [
        [
          {
            "node": "Update status in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update status in sheet": {
      "main": [
        [
          {
            "node": "Answer button",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer button": {
      "main": [
        [
          {
            "node": "Edit staff message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
