{
  "updatedAt": "2026-05-14T01:53:21.608Z",
  "createdAt": "2026-05-13T21:15:57.655Z",
  "id": "bu0kZAbLo2KUVkTl",
  "name": "Telegram School Intake → Postgres → Paperless v1.2",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "school/intake/upload",
        "responseMode": "lastNode",
        "responseData": "allEntries",
        "options": {
          "rawBody": true
        }
      },
      "id": "school-intake-webhook",
      "name": "School Intake Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -1180,
        0
      ],
      "webhookId": "6251b84e-2ac7-49a3-9b05-7ca6540da134"
    },
    {
      "parameters": {
        "jsCode": "\nconst req = $input.first().json;\nconst body = req.body || {};\nconst binary = $input.first().binary || {};\nconst upload = binary.data || Object.values(binary)[0];\nif (!upload) throw new Error('Expected multipart file upload in the webhook request.');\nconst required = ['class_name', 'assignment_name', 'submission_kind'];\nfor (const key of required) {\n  if (!body[key] || !String(body[key]).trim()) {\n    throw new Error('Missing required form field: ' + key + ' . Required fields: class_name, assignment_name, submission_kind.');\n  }\n}\nconst sanitize = (value, fallback = 'item') => String(value || fallback)\n  .trim()\n  .toLowerCase()\n  .replace(/[^a-z0-9]+/g, '-')\n  .replace(/^-+|-+$/g, '')\n  .slice(0, 80) || fallback;\nconst stableHash = (str) => {\n  let h1 = 0xdeadbeef;\n  let h2 = 0x41c6ce57;\n  for (let i = 0; i < str.length; i++) {\n    const ch = str.charCodeAt(i);\n    h1 = Math.imul(h1 ^ ch, 2654435761);\n    h2 = Math.imul(h2 ^ ch, 1597334677);\n  }\n  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n  return ((h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0'));\n};\nconst originalName = upload.fileName || body.filename || 'upload.bin';\nconst extension = (() => {\n  const m = String(originalName).match(/(\\.[A-Za-z0-9]{1,10})$/);\n  return m ? m[1].toLowerCase() : '';\n})();\nconst bytes = Number(upload.bytes ?? 0) || (() => {\n  const text = String(upload.fileSize || '').trim();\n  const m = text.match(/^([0-9.]+)\\s*(B|KB|MB|GB)$/i);\n  if (!m) return 0;\n  const value = Number(m[1]);\n  const unit = m[2].toUpperCase();\n  const mult = unit === 'GB' ? 1024**3 : unit === 'MB' ? 1024**2 : unit === 'KB' ? 1024 : 1;\n  return Number.isFinite(value) ? Math.round(value * mult) : 0;\n})();\nconst checksum = stableHash([originalName, upload.mimeType || '', String(bytes), String(body.class_name), String(body.assignment_name), String(body.submission_kind)].join('|'));\nconst now = new Date();\nconst intakeId = ['school', now.toISOString().slice(0,10).replace(/-/g,''), sanitize(body.class_name), sanitize(body.assignment_name), checksum.slice(0, 12)].join('-');\nconst storedFilename = intakeId + extension;\nconst title = [body.class_name, body.assignment_name, body.submission_kind].map(v => String(v).trim()).join(' — ');\nreturn [{\n  json: {\n    intake_id: intakeId,\n    class_name: String(body.class_name).trim(),\n    assignment_name: String(body.assignment_name).trim(),\n    submission_kind: String(body.submission_kind).trim(),\n    semester: body.semester ? String(body.semester).trim() : null,\n    course_code: body.course_code ? String(body.course_code).trim() : null,\n    paper_kind: body.paper_kind ? String(body.paper_kind).trim() : null,\n    notes: body.notes ? String(body.notes).trim().slice(0, 4000) : null,\n    source: body.source ? String(body.source).trim() : 'telegram',\n    telegram_chat_id: body.telegram_chat_id ? String(body.telegram_chat_id).trim() : null,\n    telegram_message_id: body.telegram_message_id ? String(body.telegram_message_id).trim() : null,\n    original_filename: originalName,\n    stored_filename: storedFilename,\n    mime_type: upload.mimeType || 'application/octet-stream',\n    file_size_bytes: bytes,\n    checksum_sha256: checksum,\n    paperless_title: title,\n    received_at: now.toISOString()\n  },\n  binary: {\n    data: {\n      ...upload,\n      fileName: storedFilename,\n      mimeType: upload.mimeType || 'application/octet-stream'\n    }\n  }\n}];"
      },
      "id": "normalize-request",
      "name": "Normalize Intake Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -920,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://paperless.paccoco.com/api/documents/post_document/",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Token {{ $env.PAPERLESS_API_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "document",
              "inputDataFieldName": "data"
            },
            {
              "name": "title",
              "value": "={{ $json.paperless_title }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "id": "paperless-upload",
      "name": "Upload to Paperless",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -400,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst intake = $('Normalize Intake Request').first().json;\nconst response = $json;\nconst taskId = response.task_id || response.id || response.document?.id || null;\nlet paperlessDocumentId = response.document_id || response.document?.id || null;\nif (!paperlessDocumentId && typeof response === 'object' && response?.id && !response?.task_id) {\n  paperlessDocumentId = response.id;\n}\nreturn [{ json: {\n  ...intake,\n  paperless_task_id: taskId ? String(taskId) : null,\n  paperless_document_id: paperlessDocumentId ? String(paperlessDocumentId) : null,\n  paperless_response: response,\n  uploaded_at: new Date().toISOString(),\n  status: 'uploaded'\n}}];"
      },
      "id": "capture-paperless-response",
      "name": "Capture Paperless Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -140,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nreturn [{\n  json: {\n    ok: true,\n    intake_id: $json.intake_id,\n    status: $json.status || 'uploaded',\n    paperless_task_id: $json.paperless_task_id,\n    paperless_document_id: $json.paperless_document_id,\n    paperless_title: $json.paperless_title,\n    stored_filename: $json.stored_filename,\n    received_at: $json.received_at,\n    uploaded_at: $json.uploaded_at\n  }\n}];"
      },
      "id": "success-response",
      "name": "Return Success Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        360,
        0
      ]
    }
  ],
  "connections": {
    "School Intake Webhook": {
      "main": [
        [
          {
            "node": "Normalize Intake Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Intake Request": {
      "main": [
        [
          {
            "node": "Upload to Paperless",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to Paperless": {
      "main": [
        [
          {
            "node": "Capture Paperless Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture Paperless Response": {
      "main": [
        [
          {
            "node": "Return Success Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "staticData": null,
  "meta": null,
  "pinData": {},
  "versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
  "activeVersionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
  "versionCounter": 35,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-05-13T21:15:57.655Z",
      "createdAt": "2026-05-13T21:15:57.655Z",
      "role": "workflow:owner",
      "workflowId": "bu0kZAbLo2KUVkTl",
      "projectId": "dxCRnBdX5uJizCGa",
      "project": {
        "updatedAt": "2026-05-06T01:10:37.484Z",
        "createdAt": "2026-05-06T01:08:07.721Z",
        "id": "dxCRnBdX5uJizCGa",
        "name": "Wilfred Fizzlepoof <admin@paccoco.com>",
        "type": "personal",
        "icon": null,
        "description": null,
        "creatorId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      }
    }
  ],
  "tags": [
    {
      "updatedAt": "2026-05-13T21:15:56.032Z",
      "createdAt": "2026-05-13T21:15:56.032Z",
      "id": "h1moa2N6f4A1ArxD",
      "name": "telegram"
    },
    {
      "updatedAt": "2026-05-07T02:27:25.824Z",
      "createdAt": "2026-05-07T02:27:25.824Z",
      "id": "MfaTegnw8p0SsGGm",
      "name": "postgres"
    },
    {
      "updatedAt": "2026-05-06T22:39:46.476Z",
      "createdAt": "2026-05-06T22:39:46.476Z",
      "id": "NxHjuO4MMd5kdPR7",
      "name": "paperless"
    },
    {
      "updatedAt": "2026-05-07T00:49:56.081Z",
      "createdAt": "2026-05-07T00:49:56.081Z",
      "id": "Q9lCYtCOuy9XmeyL",
      "name": "school"
    }
  ],
  "activeVersion": {
    "updatedAt": "2026-05-14T01:53:21.609Z",
    "createdAt": "2026-05-14T01:53:21.609Z",
    "versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
    "workflowId": "bu0kZAbLo2KUVkTl",
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "school/intake/upload",
          "responseMode": "lastNode",
          "responseData": "allEntries",
          "options": {
            "rawBody": true
          }
        },
        "id": "school-intake-webhook",
        "name": "School Intake Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -1180,
          0
        ],
        "webhookId": "6251b84e-2ac7-49a3-9b05-7ca6540da134"
      },
      {
        "parameters": {
          "jsCode": "\nconst req = $input.first().json;\nconst body = req.body || {};\nconst binary = $input.first().binary || {};\nconst upload = binary.data || Object.values(binary)[0];\nif (!upload) throw new Error('Expected multipart file upload in the webhook request.');\nconst required = ['class_name', 'assignment_name', 'submission_kind'];\nfor (const key of required) {\n  if (!body[key] || !String(body[key]).trim()) {\n    throw new Error('Missing required form field: ' + key + ' . Required fields: class_name, assignment_name, submission_kind.');\n  }\n}\nconst sanitize = (value, fallback = 'item') => String(value || fallback)\n  .trim()\n  .toLowerCase()\n  .replace(/[^a-z0-9]+/g, '-')\n  .replace(/^-+|-+$/g, '')\n  .slice(0, 80) || fallback;\nconst stableHash = (str) => {\n  let h1 = 0xdeadbeef;\n  let h2 = 0x41c6ce57;\n  for (let i = 0; i < str.length; i++) {\n    const ch = str.charCodeAt(i);\n    h1 = Math.imul(h1 ^ ch, 2654435761);\n    h2 = Math.imul(h2 ^ ch, 1597334677);\n  }\n  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n  return ((h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0'));\n};\nconst originalName = upload.fileName || body.filename || 'upload.bin';\nconst extension = (() => {\n  const m = String(originalName).match(/(\\.[A-Za-z0-9]{1,10})$/);\n  return m ? m[1].toLowerCase() : '';\n})();\nconst bytes = Number(upload.bytes ?? 0) || (() => {\n  const text = String(upload.fileSize || '').trim();\n  const m = text.match(/^([0-9.]+)\\s*(B|KB|MB|GB)$/i);\n  if (!m) return 0;\n  const value = Number(m[1]);\n  const unit = m[2].toUpperCase();\n  const mult = unit === 'GB' ? 1024**3 : unit === 'MB' ? 1024**2 : unit === 'KB' ? 1024 : 1;\n  return Number.isFinite(value) ? Math.round(value * mult) : 0;\n})();\nconst checksum = stableHash([originalName, upload.mimeType || '', String(bytes), String(body.class_name), String(body.assignment_name), String(body.submission_kind)].join('|'));\nconst now = new Date();\nconst intakeId = ['school', now.toISOString().slice(0,10).replace(/-/g,''), sanitize(body.class_name), sanitize(body.assignment_name), checksum.slice(0, 12)].join('-');\nconst storedFilename = intakeId + extension;\nconst title = [body.class_name, body.assignment_name, body.submission_kind].map(v => String(v).trim()).join(' — ');\nreturn [{\n  json: {\n    intake_id: intakeId,\n    class_name: String(body.class_name).trim(),\n    assignment_name: String(body.assignment_name).trim(),\n    submission_kind: String(body.submission_kind).trim(),\n    semester: body.semester ? String(body.semester).trim() : null,\n    course_code: body.course_code ? String(body.course_code).trim() : null,\n    paper_kind: body.paper_kind ? String(body.paper_kind).trim() : null,\n    notes: body.notes ? String(body.notes).trim().slice(0, 4000) : null,\n    source: body.source ? String(body.source).trim() : 'telegram',\n    telegram_chat_id: body.telegram_chat_id ? String(body.telegram_chat_id).trim() : null,\n    telegram_message_id: body.telegram_message_id ? String(body.telegram_message_id).trim() : null,\n    original_filename: originalName,\n    stored_filename: storedFilename,\n    mime_type: upload.mimeType || 'application/octet-stream',\n    file_size_bytes: bytes,\n    checksum_sha256: checksum,\n    paperless_title: title,\n    received_at: now.toISOString()\n  },\n  binary: {\n    data: {\n      ...upload,\n      fileName: storedFilename,\n      mimeType: upload.mimeType || 'application/octet-stream'\n    }\n  }\n}];"
        },
        "id": "normalize-request",
        "name": "Normalize Intake Request",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -920,
          0
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://paperless.paccoco.com/api/documents/post_document/",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "=Token {{ $env.PAPERLESS_API_TOKEN }}"
              }
            ]
          },
          "sendBody": true,
          "contentType": "multipart-form-data",
          "bodyParameters": {
            "parameters": [
              {
                "parameterType": "formBinaryData",
                "name": "document",
                "inputDataFieldName": "data"
              },
              {
                "name": "title",
                "value": "={{ $json.paperless_title }}"
              }
            ]
          },
          "options": {
            "response": {
              "response": {
                "responseFormat": "text"
              }
            }
          }
        },
        "id": "paperless-upload",
        "name": "Upload to Paperless",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -400,
          0
        ]
      },
      {
        "parameters": {
          "jsCode": "\nconst intake = $('Normalize Intake Request').first().json;\nconst response = $json;\nconst taskId = response.task_id || response.id || response.document?.id || null;\nlet paperlessDocumentId = response.document_id || response.document?.id || null;\nif (!paperlessDocumentId && typeof response === 'object' && response?.id && !response?.task_id) {\n  paperlessDocumentId = response.id;\n}\nreturn [{ json: {\n  ...intake,\n  paperless_task_id: taskId ? String(taskId) : null,\n  paperless_document_id: paperlessDocumentId ? String(paperlessDocumentId) : null,\n  paperless_response: response,\n  uploaded_at: new Date().toISOString(),\n  status: 'uploaded'\n}}];"
        },
        "id": "capture-paperless-response",
        "name": "Capture Paperless Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -140,
          0
        ]
      },
      {
        "parameters": {
          "jsCode": "\nreturn [{\n  json: {\n    ok: true,\n    intake_id: $json.intake_id,\n    status: $json.status || 'uploaded',\n    paperless_task_id: $json.paperless_task_id,\n    paperless_document_id: $json.paperless_document_id,\n    paperless_title: $json.paperless_title,\n    stored_filename: $json.stored_filename,\n    received_at: $json.received_at,\n    uploaded_at: $json.uploaded_at\n  }\n}];"
        },
        "id": "success-response",
        "name": "Return Success Payload",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          360,
          0
        ]
      }
    ],
    "connections": {
      "School Intake Webhook": {
        "main": [
          [
            {
              "node": "Normalize Intake Request",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Normalize Intake Request": {
        "main": [
          [
            {
              "node": "Upload to Paperless",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Upload to Paperless": {
        "main": [
          [
            {
              "node": "Capture Paperless Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Capture Paperless Response": {
        "main": [
          [
            {
              "node": "Return Success Payload",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "Wilfred Fizzlepoof",
    "name": null,
    "description": null,
    "autosaved": false,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-05-14T01:53:21.640Z",
        "id": 258,
        "workflowId": "bu0kZAbLo2KUVkTl",
        "versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
        "event": "deactivated",
        "userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      },
      {
        "createdAt": "2026-05-14T01:53:21.661Z",
        "id": 259,
        "workflowId": "bu0kZAbLo2KUVkTl",
        "versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
        "event": "activated",
        "userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
      }
    ]
  }
}
