{
  "name": "Telegram School Intake → Postgres → Paperless v1.1",
  "active": false,
  "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
      ]
    },
    {
      "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 fileBytes = Buffer.from(upload.data, 'base64');\nconst checksum = stableHash([originalName, upload.mimeType || '', String(fileBytes.length), upload.data].join('|'));\nconst now = new Date();\nconst intakeId = [\n  'school',\n  now.toISOString().slice(0,10).replace(/-/g,''),\n  sanitize(body.class_name),\n  sanitize(body.assignment_name),\n  checksum.slice(0, 12)\n].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: fileBytes.length,\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": {
        "operation": "executeQuery",
        "query": "INSERT INTO school_paperless_intake (\n  intake_id,\n  class_name,\n  assignment_name,\n  submission_kind,\n  semester,\n  course_code,\n  paper_kind,\n  notes,\n  source,\n  telegram_chat_id,\n  telegram_message_id,\n  original_filename,\n  stored_filename,\n  mime_type,\n  file_size_bytes,\n  checksum_sha256,\n  status,\n  received_at\n)\nVALUES (\n  '{{ $json.intake_id }}',\n  '{{ $json.class_name.replace(/'/g, \"''\") }}',\n  '{{ $json.assignment_name.replace(/'/g, \"''\") }}',\n  '{{ $json.submission_kind.replace(/'/g, \"''\") }}',\n  {{ $json.semester ? \"'\" + $json.semester.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  {{ $json.course_code ? \"'\" + $json.course_code.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  {{ $json.paper_kind ? \"'\" + $json.paper_kind.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  {{ $json.notes ? \"'\" + $json.notes.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  '{{ $json.source.replace(/'/g, \"''\") }}',\n  {{ $json.telegram_chat_id ? \"'\" + $json.telegram_chat_id.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  {{ $json.telegram_message_id ? \"'\" + $json.telegram_message_id.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  '{{ $json.original_filename.replace(/'/g, \"''\") }}',\n  '{{ $json.stored_filename.replace(/'/g, \"''\") }}',\n  '{{ $json.mime_type.replace(/'/g, \"''\") }}',\n  {{ $json.file_size_bytes }},\n  '{{ $json.checksum_sha256 }}',\n  'received',\n  '{{ $json.received_at }}'::timestamptz\n)\nON CONFLICT (intake_id) DO UPDATE SET\n  class_name = EXCLUDED.class_name,\n  assignment_name = EXCLUDED.assignment_name,\n  submission_kind = EXCLUDED.submission_kind,\n  semester = EXCLUDED.semester,\n  course_code = EXCLUDED.course_code,\n  paper_kind = EXCLUDED.paper_kind,\n  notes = EXCLUDED.notes,\n  source = EXCLUDED.source,\n  telegram_chat_id = EXCLUDED.telegram_chat_id,\n  telegram_message_id = EXCLUDED.telegram_message_id,\n  original_filename = EXCLUDED.original_filename,\n  stored_filename = EXCLUDED.stored_filename,\n  mime_type = EXCLUDED.mime_type,\n  file_size_bytes = EXCLUDED.file_size_bytes,\n  checksum_sha256 = EXCLUDED.checksum_sha256,\n  status = 'received',\n  received_at = EXCLUDED.received_at,\n  updated_at = NOW();"
      },
      "id": "pg-insert-received",
      "name": "Postgres Upsert Intake Received",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -660,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "SETUP_REQUIRED",
          "name": "Shared Postgres"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '') + '/api/documents/post_document/'}}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "document",
              "value": "data",
              "parameterType": "formBinaryData"
            },
            {
              "name": "title",
              "value": "={{$json.paperless_title}}"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "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}}];"
      },
      "id": "capture-paperless-response",
      "name": "Capture Paperless Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -140,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE school_paperless_intake\nSET\n  paperless_task_id = {{ $json.paperless_task_id ? \"'\" + $json.paperless_task_id.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  paperless_document_id = {{ $json.paperless_document_id ? \"'\" + $json.paperless_document_id.replace(/'/g, \"''\") + \"'\" : 'NULL' }},\n  paperless_title = '{{ $json.paperless_title.replace(/'/g, \"''\") }}',\n  status = 'uploaded',\n  uploaded_at = '{{ $json.uploaded_at }}'::timestamptz,\n  updated_at = NOW()\nWHERE intake_id = '{{ $json.intake_id }}';"
      },
      "id": "pg-mark-uploaded",
      "name": "Postgres Mark Uploaded",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        120,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "SETUP_REQUIRED",
          "name": "Shared Postgres"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "\nreturn [{\n  json: {\n    ok: true,\n    intake_id: $json.intake_id,\n    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": "Postgres Upsert Intake Received",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Upsert Intake Received": {
      "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": "Postgres Mark Uploaded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Mark Uploaded": {
      "main": [
        [
          {
            "node": "Return Success Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": false,
    "artifactVersion": "v1.1"
  },
  "pinData": {},
  "tags": [
    {
      "name": "school"
    },
    {
      "name": "paperless"
    },
    {
      "name": "postgres"
    },
    {
      "name": "telegram"
    }
  ]
}
