{
  "name": "Rechnungs-PDF an bimetrics übertragen",
  "active": false,
  "nodes": [
    {
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "id": "7b834aa0-22c9-557d-b805-c1949ae5defe",
      "name": "Manuell starten",
      "position": [
        0,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Trage die PDF auf dem n8n-Server und eine stabile ID aus deinem Quellsystem ein.\n// Für einen neuen Beleg eine neue ID; bei Wiederholungen dieselbe ID behalten.\nconst input = {\n  filePath: '',\n  fileName: '',\n  sourceDocumentId: '',\n};\n\nconst filePath = input.filePath.trim();\nconst fileName = input.fileName.trim();\nconst sourceDocumentId = input.sourceDocumentId.trim();\nconst missing = [\n  !filePath && 'filePath (absoluter Dateipfad auf dem n8n-Server)',\n  !fileName && 'fileName (Dateiname mit .pdf)',\n  !sourceDocumentId && 'sourceDocumentId (stabile ID aus deinem Quellsystem)',\n].filter(Boolean);\nif (missing.length) {\n  throw new Error('Vor dem Upload im Node „Upload konfigurieren“ ausfüllen: ' + missing.join(', ') + '. Es wurde kein API-Request gesendet.');\n}\nif ([filePath, fileName, sourceDocumentId].some(value => /BITTE_|HIER_EINTRAGEN|YOUR_|<[^>]*>/i.test(value))) {\n  throw new Error('Ersetze alle Platzhalter in „Upload konfigurieren“. Es wurde kein API-Request gesendet.');\n}\nif (!(/^(?:\\/|[A-Za-z]:[\\\\/])/.test(filePath)) || /[*?\\[\\]{}]/.test(filePath)) {\n  throw new Error('filePath muss ein absoluter Serverpfad zu genau einer Datei sein, ohne Platzhalter für mehrere Dateien.');\n}\nif (!/\\.pdf$/i.test(fileName) || /[\\\\/\\x00-\\x1f]/.test(fileName)) {\n  throw new Error('fileName muss ein einzelner PDF-Dateiname ohne Verzeichnispfad sein.');\n}\nif (!/^[!-~]{1,128}$/.test(sourceDocumentId)) {\n  throw new Error('sourceDocumentId braucht 1 bis 128 druckbare ASCII-Zeichen ohne Leerzeichen. Die ID bleibt bei Wiederholungen gleich.');\n}\n\nreturn [{json: {\n  baseUrl: 'https://app.bimetrics.de',\n  filePath,\n  fileName,\n  sourceDocumentId,\n  uploadRequestId: sourceDocumentId,\n  maxStatusRequests: 6,\n  pollIntervalSeconds: 5,\n}}];"
      },
      "id": "fde80a34-7db1-5d19-afec-36adc34488c7",
      "name": "Upload konfigurieren",
      "position": [
        220,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "parameters": {
        "operation": "read",
        "fileSelector": "={{ $json.filePath }}",
        "options": {
          "dataPropertyName": "data",
          "fileName": "={{ $json.fileName }}",
          "mimeType": "application/pdf"
        }
      },
      "id": "736e7c17-644b-5d3c-a3c3-d6f3a4901c7c",
      "name": "Vorhandene PDF lesen",
      "position": [
        440,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1 || !items[0].binary?.data) {\n  throw new Error('Erwartet wird genau eine PDF als Binärdatei im Feld data. Prüfe den Dateipfad auf dem n8n-Server.');\n}\nconst cfg = $('Upload konfigurieren').first().json;\nreturn [{json: cfg, binary: {data: {\n  ...items[0].binary.data,\n  fileName: cfg.fileName,\n  mimeType: 'application/pdf',\n}}}];"
      },
      "id": "efb932b1-245a-5be3-999a-49f17007044f",
      "name": "Datei prüfen",
      "position": [
        660,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "parameters": {
        "method": "POST",
        "url": "={{ $('Upload konfigurieren').first().json.baseUrl + '/api/document/' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          },
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Upload-Request-Id",
              "value": "={{ $('Upload konfigurieren').first().json.uploadRequestId }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "receipt[]",
              "inputDataFieldName": "data"
            }
          ]
        }
      },
      "id": "335517a3-c214-516d-8397-75f66c3f92d2",
      "name": "PDF hochladen",
      "position": [
        880,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const response = $input.first().json;\nconst status = Number(response.statusCode);\nif (!(status >= 200 && status < 300)) {\n  const code = typeof response.body?.code === 'string' ? response.body.code : 'UnknownAPIError';\n  const retryAfter = response.headers?.['retry-after'] ?? null;\n  const instruction = status === 429\n    ? 'Warte die Sekunden aus Retry-After. Wiederhole nur mit derselben Firma, demselben Schlüssel, derselben Upload-ID, Datei, demselben Dateinamen und Feld.'\n    : status >= 500\n      ? 'Vorübergehender Serverfehler. Wiederhole mit begrenzten Versuchen und wachsendem Abstand; die gesamte Upload-Identität muss gleich bleiben.'\n      : 'Prüfe Anfrage, Schlüssel, Berechtigungen und Tarif. Keine automatische Wiederholung.';\n  throw new Error(JSON.stringify({phase: 'upload', httpStatus: status, code, retryAfter, instruction}));\n}\nif (!Array.isArray(response.body) || response.body.length !== 1 || typeof response.body[0].id !== 'string' || !response.body[0].id) {\n  throw new Error('Die Upload-Antwort enthält nicht genau einen Beleg mit Beleg-ID. Prüfe die Antwort, bevor du den Upload wiederholst.');\n}\nconst document = response.body[0];\nreturn [{json: {\n  documentId: document.id,\n  uploadStatus: document.uploadDocument?.ocrStatus ?? null,\n  duplicate: document.duplicate === true,\n  uploadRequestId: $('Upload konfigurieren').first().json.uploadRequestId,\n}}];"
      },
      "id": "fbfdce17-04da-5da5-a260-aad552427b55",
      "name": "Uploadantwort prüfen",
      "position": [
        1100,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "parameters": {
        "method": "GET",
        "url": "={{ $('Upload konfigurieren').first().json.baseUrl + '/api/document/' + encodeURIComponent($json.documentId) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          },
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        }
      },
      "id": "0e5d05ab-535a-557c-ae2b-c20a3efd1d37",
      "name": "Verarbeitungsstatus lesen",
      "position": [
        1320,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const first = $('Uploadantwort prüfen').first().json;\nconst response = $input.first().json;\nconst status = Number(response.statusCode);\nif (!(status >= 200 && status < 300)) {\n  const code = typeof response.body?.code === 'string' ? response.body.code : 'UnknownAPIError';\n  const retryAfter = response.headers?.['retry-after'] ?? null;\n  const instruction = status === 429\n    ? 'Warte die Sekunden aus Retry-After und frage diese Beleg-ID später erneut ab. Kein erneuter Upload nötig.'\n    : status >= 500\n      ? 'Frage den Status dieser Beleg-ID später mit wachsendem Abstand erneut ab. Kein erneuter Upload nötig.'\n      : 'Der Upload hat bereits eine Beleg-ID geliefert. Prüfe Schlüssel und Leseberechtigung; nutze diese ID für weitere Statusabfragen.';\n  throw new Error(JSON.stringify({phase: 'read-status', documentId: first.documentId, httpStatus: status, code, retryAfter, instruction}));\n}\nif (response.body?.id !== first.documentId) {\n  throw new Error('Die Statusantwort enthält eine unerwartete Beleg-ID.');\n}\nconst cfg = $('Upload konfigurieren').first().json;\nconst statusValue = response.body.uploadDocument?.ocrStatus;\nif (!['pending', 'processing', 'success', 'failed', 'quota_paused'].includes(statusValue)) {\n  throw new Error('Die API hat keinen bekannten Verarbeitungsstatus geliefert. Behalte die Beleg-ID aus „Uploadantwort prüfen“ und prüfe die Antwort.');\n}\nconst statusRequests = $runIndex + 1;\nconst shouldPoll = ['pending', 'processing'].includes(statusValue) && statusRequests < cfg.maxStatusRequests;\nconst outcome = statusValue === 'success' ? 'processed'\n  : statusValue === 'failed' ? 'processing_failed'\n  : statusValue === 'quota_paused' ? 'quota_paused'\n  : shouldPoll ? 'pending' : 'poll_limit_reached';\nconst nextAction = outcome === 'processed'\n  ? 'Die API meldet die Verarbeitung als abgeschlossen. Öffne den Beleg in bimetrics und prüfe die Angaben; dieser Status ist keine Buchungsfreigabe.'\n  : outcome === 'quota_paused'\n    ? 'Prüfe das Belegkontingent und diesen gespeicherten Beleg. Lade die Datei nicht unter einer neuen ID erneut hoch.'\n    : outcome === 'processing_failed'\n      ? 'Öffne diesen gespeicherten Beleg in bimetrics und prüfe den Verarbeitungsfehler. Kein erneuter Upload unter einer neuen ID.'\n      : outcome === 'poll_limit_reached'\n        ? 'Die Verarbeitung ist noch offen. Behalte die Beleg-ID und frage den Status später erneut ab; kein neuer Upload.'\n        : 'Warte fünf Sekunden bis zur nächsten Statusabfrage.';\nreturn [{json: {...first, ocrStatus: statusValue, statusRequests, shouldPoll, outcome, nextAction}}];"
      },
      "id": "f0e1a36c-963a-561c-ba1e-d25f1db00f6f",
      "name": "Status einordnen",
      "position": [
        1540,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "status-poll-required",
              "leftValue": "={{ $json.shouldPoll }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b000475a-74a7-546c-a64d-6269b056e195",
      "name": "Weitere Abfrage nötig?",
      "position": [
        1760,
        0
      ]
    },
    {
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "parameters": {
        "resume": "timeInterval",
        "amount": "={{ $('Upload konfigurieren').first().json.pollIntervalSeconds }}",
        "unit": "seconds"
      },
      "id": "2c4f54d7-83ae-5726-9ec0-2a4ded2be5e7",
      "name": "5 Sekunden warten",
      "position": [
        1980,
        -160
      ]
    },
    {
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return $input.all();"
      },
      "id": "cb63a002-130f-53c4-bafe-6a536e1fc161",
      "name": "Ergebnis",
      "position": [
        1980,
        140
      ]
    },
    {
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Vorhandene PDF an bimetrics übertragen\nFür selbst gehostetes n8n · Parameterbasis: n8n 2.40.7 / HTTP Request 4.5.\n\n1. In „Upload konfigurieren“ Serverpfad, PDF-Dateiname und stabile Quell-ID eintragen. Ohne diese Angaben stoppt der Ablauf vor dem Request. Der Dateipfad gilt innerhalb der n8n-Laufzeit; bei Docker muss die Datei im Container verfügbar sein.\n2. In beiden HTTP-Nodes dasselbe Header-Auth-Credential wählen: Header Authorization, Wert Bearer gefolgt vom API-Schlüssel. Scopes: upload + read. API-Zugang der gewählten Firma erforderlich.\n\nEin POST mit receipt[]; danach höchstens sechs Status-GETs, mit fünf Sekunden Abstand. HTTP-Fehler stoppen. Kein automatischer Upload-Retry.\nBei einer zulässigen Wiederholung Firma, Schlüssel, Upload-ID, Datei, Dateiname und Feld beibehalten. Für einen neuen Beleg eine neue Quell-ID verwenden.\n\nsuccess ist der gemeldete API-Zustand, keine Buchungsfreigabe. Angaben anschließend in bimetrics prüfen.\nAPI-Vertrag: https://developer.bimetrics.de",
        "height": 430,
        "width": 1040
      },
      "id": "7d774ee0-3b7e-5e0f-a5e0-eacae72d4eb2",
      "name": "Einrichtung und Umfang",
      "position": [
        220,
        -510
      ]
    }
  ],
  "connections": {
    "Manuell starten": {
      "main": [
        [
          {
            "node": "Upload konfigurieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload konfigurieren": {
      "main": [
        [
          {
            "node": "Vorhandene PDF lesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vorhandene PDF lesen": {
      "main": [
        [
          {
            "node": "Datei prüfen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Datei prüfen": {
      "main": [
        [
          {
            "node": "PDF hochladen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PDF hochladen": {
      "main": [
        [
          {
            "node": "Uploadantwort prüfen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Uploadantwort prüfen": {
      "main": [
        [
          {
            "node": "Verarbeitungsstatus lesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verarbeitungsstatus lesen": {
      "main": [
        [
          {
            "node": "Status einordnen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Status einordnen": {
      "main": [
        [
          {
            "node": "Weitere Abfrage nötig?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weitere Abfrage nötig?": {
      "main": [
        [
          {
            "node": "5 Sekunden warten",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ergebnis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5 Sekunden warten": {
      "main": [
        [
          {
            "node": "Verarbeitungsstatus lesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 240,
    "saveExecutionProgress": false
  },
  "pinData": {},
  "tags": []
}
