{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ue0-antragspdf",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "dd676723-4a94-4478-9ba6-3cde6e8c982f",
      "name": "Webhook Trigger (Supabase NOTIFY)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        208,
        304
      ]
    },
    {
      "parameters": {
        "url": "=http://kong:8000/storage/v1/object/antragseingang-pdf/{{ $json.body.storage_path }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "1226131a-fad0-430b-a0b3-becdbf7a4933",
      "name": "PDF aus Storage holen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        416,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Holt das Binary-PDF als Buffer und gibt es als reinen Base64-String weiter.\n// Funktioniert mit n8n's binary storage modes 'default' (in-memory base64),\n// 'filesystem' (file-ref), und 's3' (URL).\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst b64 = buffer.toString('base64');\nconst bin = $input.first().binary.data;\nreturn [{ json: { pdf_base64: b64, mime_type: bin.mimeType || 'application/pdf' } }];"
      },
      "id": "encode-1",
      "name": "PDF \u2192 Base64",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-sonnet-4-5\",\n  \"max_tokens\": 4096,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"document\",\n          \"source\": {\n            \"type\": \"base64\",\n            \"media_type\": \"application/pdf\",\n            \"data\": \"{{ $json.pdf_base64 }}\"\n          }\n        },\n        {\n          \"type\": \"text\",\n          \"text\": \"Du bekommst ein ausgef\u00fclltes Antragsformular der Stadt W\u00fcrzburg (Altenhilfeplan Nr. 2, Antrag auf Zuschuss f\u00fcr Altentagesst\u00e4tten). Das Formular kann maschinell ausgef\u00fcllt sein ODER handschriftlich. Extrahiere ALLE ausgef\u00fcllten Felder als striktes JSON nach folgendem Schema. Wenn ein Feld leer/nicht lesbar ist, setze null. Bei Betr\u00e4gen: nur die Zahl als float (z.B. 1250.50), keine W\u00e4hrung, kein Tausender-Punkt. IBAN/BIC trimmen, ohne Leerzeichen. Antwort NUR als JSON-Objekt, keine Erkl\u00e4rung davor oder danach, kein Code-Fence. WICHTIG zur Pflicht: die Verwaltungspraxis der Stadt W\u00fcrzburg verlangt Telefon, Bankverbindung (Bankname) und BIC IMMER \u2014 auch wenn das PDF kein Sternchen zeigt. Wenn du die Felder im PDF nicht findest, suche sorgf\u00e4ltig (h\u00e4ufig handschriftlich am Rand oder im Briefkopf).\\n\\nSchema:\\n{\\n  \\\"haushaltsjahr\\\": integer | null,\\n  \\\"name\\\": string | null,                    // Name der Einrichtung\\n  \\\"traeger\\\": string | null,                 // Tr\u00e4gerverein/Organisation\\n  \\\"strasse\\\": string | null,\\n  \\\"hausnummer\\\": string | null,\\n  \\\"plz\\\": string | null,                      // 5-stellig\\n  \\\"ort\\\": string | null,\\n  \\\"bankverbindung\\\": string | null,           // Bankname \u2014 Verwaltungspraxis: Pflicht\\n  \\\"iban\\\": string | null,\\n  \\\"bic\\\": string | null,                      // Verwaltungspraxis: Pflicht (auch DE-IBAN)\\n  \\\"ansprechpartner\\\": string | null,\\n  \\\"telefon\\\": string | null,                  // Verwaltungspraxis: Pflicht (f\u00fcr R\u00fcckfragen)\\n  \\\"email\\\": string | null,\\n  \\\"betriebskosten_vorjahr_euro\\\": float | null,\\n  \\\"personalkosten_vorjahr_euro\\\": float | null,\\n  \\\"monatliche_miete_euro\\\": float | null,     // PDF fragt MONATLICH ab; nicht selbst \u00d7 12 rechnen\\n  \\\"raeume_vorhanden\\\": \\\"ja\\\" | \\\"nein\\\" | null,\\n  \\\"raeume_unentgeltlich\\\": \\\"ja\\\" | \\\"nein\\\" | null,\\n  \\\"antragsdatum\\\": string | null              // ISO YYYY-MM-DD\\n}\"\n        }\n      ]\n    }\n  ]\n}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "e6ccef34-12b8-4ccb-acbe-d2a7b6c2772a",
      "name": "Claude Vision \u2014 OCR + Extraktion",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Final-Sweep 2026-05-24: OCR-Daten werden NUR in antrag_einreichung.\n// extrahiert_jsonb geparkt. Apl2.antraege erst beim finalen B\u00fcrger-Submit\n// aus UE1 bef\u00fcllt \u2014 saubere Vorlage-vs-Antrag-Trennung.\nconst input = $input.first().json;\nconst content = input.content?.[0]?.text;\nif (!content) throw new Error('Claude lieferte keinen Text-Content: ' + JSON.stringify(input));\nlet extr;\ntry {\n  const cleaned = content.trim().replace(/^```json\\n?/, '').replace(/\\n?```$/, '');\n  extr = JSON.parse(cleaned);\n} catch (e) {\n  throw new Error('Claude-Antwort ist kein JSON: ' + content.slice(0, 500));\n}\n// Defaults f\u00fcr UE1-Prefill: Status + Sprache\nextr.submitted_language = 'de';\nif (!extr.antragsdatum) extr.antragsdatum = new Date().toISOString().slice(0, 10);\n// foerderbereich + geforderte_foerdersumme bleiben null \u2014 Sachbearbeitung beziffert\n// Bemessungsfelder bleiben null \u2014 PDF fragt sie nicht ab\n// oeffnungszeiten kommen aus dem Wochenplan-Branch (separater Node merged sie rein)\nreturn [{\n  json: {\n    extrahiert: extr,\n    einreichung_id: $('Webhook Trigger (Supabase NOTIFY)').first().json.body.einreichung_id\n  }\n}];"
      },
      "id": "aac56805-0923-4e36-b7dd-ab7388620f35",
      "name": "Parse JSON + Defaults",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        944,
        304
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=http://kong:8000/rest/v1/antrag_einreichung?id=eq.{{ $('Parse JSON + Defaults').first().json.einreichung_id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept-Profile",
              "value": "apl2"
            },
            {
              "name": "Content-Profile",
              "value": "apl2"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"status\": \"fertig\",\n  \"extrahiert_jsonb\": {{ JSON.stringify($json.extrahiert) }},\n  \"verarbeitet_am\": \"{{ new Date().toISOString() }}\"\n}",
        "options": {}
      },
      "id": "26e94168-7d4c-4b44-95cb-82592bb064d8",
      "name": "UPDATE antrag_einreichung status=fertig",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2080,
        208
      ]
    },
    {
      "id": "if-anlage-exists",
      "name": "IF Anlage 1 vorhanden?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [
        928,
        504
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond-1",
              "leftValue": "={{ $('Webhook Trigger (Supabase NOTIFY)').first().json.body.anlage_1_storage_path }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "anlage-1-storage",
      "name": "Anlage-1 aus Storage holen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1148,
        444
      ],
      "parameters": {
        "url": "=http://kong:8000/storage/v1/object/antragseingang-pdf/{{ $('Webhook Trigger (Supabase NOTIFY)').first().json.body.anlage_1_storage_path }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      }
    },
    {
      "id": "anlage-1-b64",
      "name": "Anlage-1 \u2192 Base64",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1368,
        444
      ],
      "parameters": {
        "jsCode": "// Gleich wie Hauptantrag-B64, aber f\u00fcr den Anlage-1-PDF-Buffer.\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst b64 = buffer.toString('base64');\nconst bin = $input.first().binary.data;\nreturn [{ json: { pdf_base64: b64, mime_type: bin.mimeType || 'application/pdf' } }];"
      }
    },
    {
      "id": "anlage-1-claude",
      "name": "Claude Vision \u2014 Wochenplan-OCR",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1588,
        444
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-sonnet-4-5\",\n  \"max_tokens\": 2048,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"document\",\n          \"source\": {\n            \"type\": \"base64\",\n            \"media_type\": \"application/pdf\",\n            \"data\": \"{{ $json.pdf_base64 }}\"\n          }\n        },\n        {\n          \"type\": \"text\",\n          \"text\": \"Du bekommst ein ausgef\u00fclltes 'Anlage 1' zum Antrag Altenhilfeplan Nr. 2 der Stadt W\u00fcrzburg. Es ist eine Wochenplan-Tabelle mit den Spalten 'Wochentag', '\u00d6ffnungszeiten' und 'Angebot'. Die Eintr\u00e4ge k\u00f6nnen maschinell oder handschriftlich sein. Extrahiere ALLE ausgef\u00fcllten Zeilen als JSON-Array.\\n\\nFormat pro Zeile: {\\\"wochentag\\\": \\\"mo|di|mi|do|fr|sa|so\\\", \\\"oeffnungszeit\\\": \\\"<Original-Text z.B. '09:30 \u2013 11:30'>\\\", \\\"angebot\\\": \\\"<Original-Text>\\\"}\\n\\nWICHTIG:\\n- 'mo' f\u00fcr Montag, 'di' f\u00fcr Dienstag, 'mi' f\u00fcr Mittwoch, 'do' f\u00fcr Donnerstag, 'fr' f\u00fcr Freitag, 'sa' f\u00fcr Samstag, 'so' f\u00fcr Sonntag.\\n- Wenn eine Zelle (\u00d6ffnungszeit ODER Angebot) leer ist, lass den Eintrag WEG (nicht 'null' ausgeben, sondern komplett \u00fcberspringen).\\n- Bei handschriftlichen Eintr\u00e4gen: erkenne so gut wie m\u00f6glich. Bei totaler Unlesbarkeit kann das Feld leer bleiben (und somit die Zeile wegfallen).\\n- Erfinde NICHTS. Wenn das PDF keine Wochenplan-Tabelle enth\u00e4lt, gib [] zur\u00fcck.\\n\\nAntworte mit REINEM JSON (Array). Kein Flie\u00dftext, keine Markdown-Code-Fence.\"\n        }\n      ]\n    }\n  ]\n}",
        "options": {
          "timeout": 120000
        }
      }
    },
    {
      "id": "parse-wochenplan-merge",
      "name": "Parse Wochenplan + Merge in extrahiert",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1808,
        444
      ],
      "parameters": {
        "jsCode": "// Parsed Claude-Antwort (JSON-Array von Wochenplan-Eintr\u00e4gen) und merged\n// sie in extrahiert.oeffnungszeiten. Die Hauptantrag-Daten kommen vom\n// Parse-JSON-Defaults-Node (\u00fcber $()-Referenz), nicht vom direkten Input.\nconst claude = $input.first().json;\nconst content = claude.content?.[0]?.text;\nif (!content) throw new Error('Claude(Wochenplan) lieferte keinen Text: ' + JSON.stringify(claude));\nlet oeffnungszeiten;\ntry {\n  const cleaned = content.trim().replace(/^```json\\n?/, '').replace(/\\n?```$/, '');\n  oeffnungszeiten = JSON.parse(cleaned);\n  if (!Array.isArray(oeffnungszeiten)) {\n    throw new Error('Claude lieferte kein Array, sondern: ' + typeof oeffnungszeiten);\n  }\n} catch (e) {\n  // Soft-Fail: leerer Wochenplan, Hauptantrag-Daten gehen trotzdem durch.\n  console.error('Wochenplan-Parse fehlgeschlagen: ' + e.message + ' \u2014 Content: ' + content.slice(0, 300));\n  oeffnungszeiten = [];\n}\nconst parseNode = $('Parse JSON + Defaults').first().json;\nconst extrahiert = { ...parseNode.extrahiert, oeffnungszeiten };\nreturn [{\n  json: {\n    extrahiert,\n    einreichung_id: parseNode.einreichung_id,\n  }\n}];"
      }
    }
  ],
  "connections": {
    "Webhook Trigger (Supabase NOTIFY)": {
      "main": [
        [
          {
            "node": "PDF aus Storage holen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PDF aus Storage holen": {
      "main": [
        [
          {
            "node": "PDF \u2192 Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PDF \u2192 Base64": {
      "main": [
        [
          {
            "node": "Claude Vision \u2014 OCR + Extraktion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Vision \u2014 OCR + Extraktion": {
      "main": [
        [
          {
            "node": "Parse JSON + Defaults",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON + Defaults": {
      "main": [
        [
          {
            "node": "IF Anlage 1 vorhanden?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Anlage 1 vorhanden?": {
      "main": [
        [
          {
            "node": "Anlage-1 aus Storage holen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "UPDATE antrag_einreichung status=fertig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anlage-1 aus Storage holen": {
      "main": [
        [
          {
            "node": "Anlage-1 \u2192 Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anlage-1 \u2192 Base64": {
      "main": [
        [
          {
            "node": "Claude Vision \u2014 Wochenplan-OCR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Vision \u2014 Wochenplan-OCR": {
      "main": [
        [
          {
            "node": "Parse Wochenplan + Merge in extrahiert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Wochenplan + Merge in extrahiert": {
      "main": [
        [
          {
            "node": "UPDATE antrag_einreichung status=fertig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}