{
  "name": "01_Beleg_Eingang_und_OCR",
  "nodes": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "Webhook: Beleg Upload",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [200, 300],
      "parameters": {
        "httpMethod": "POST",
        "path": "beleg-upload",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true,
          "binaryPropertyName": "data"
        }
      }
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Code: Normalisierung",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [440, 300],
      "parameters": {
27 +        "jsCode": "const item = $input.first();\n\n// Testmodus: JSON mit testmodus:true → direkt weiterleiten\nif (item.json && item.json.testmodus === true) 
         +{\n  return [{\n    json: {\n      ...item.json,\n      quelle: 'testmodus',\n      eingang_timestamp: new Date().toISOString()\n    }\n  }];\n}\n\n// Produkti
         +onspfad: Datei aus Quelle extrahieren\nlet fileBase64 = null;\nlet mimeType = 'application/pdf';\nlet originalName = 'unbekannt.pdf';\nlet quelle = 'unbekannt'
         +;\n\n// Fall 1: Webhook mit Binary-Upload\nif (item.binary && Object.keys(item.binary).length > 0) {\n  const binaryKey = Object.keys(item.binary)[0];\n  const
         + bin = item.binary[binaryKey];\n  mimeType = bin.mimeType || 'application/pdf';\n  originalName = bin.fileName || `upload.${bin.fileExtension || 'pdf'}`;\n  qu
         +elle = 'webhook';\n  // n8n v2 speichert Binärdaten als filesystem-Referenz → Buffer holen\n  const buffer = await this.helpers.getBinaryDataBuffer(item, binar
         +yKey);\n  fileBase64 = buffer.toString('base64');\n}\n// Fall 2: E-Mail-Anhang\nelse if (item.json.attachments && item.json.attachments.length > 0) {\n  const 
         +anhang = item.json.attachments[0];\n  fileBase64 = anhang.content;\n  mimeType = anhang.contentType || 'application/pdf';\n  originalName = anhang.filename || 
         +'email_anhang.pdf';\n  quelle = 'email';\n}\n// Fall 3: Google Drive\nelse if (item.json.id) {\n  const binaryKey = Object.keys(item.binary || {})[0];\n  if (b
         +inaryKey) {\n    mimeType = item.binary[binaryKey].mimeType || 'application/pdf';\n    const buffer = await this.helpers.getBinaryDataBuffer(item, binaryKey);\
         +n    fileBase64 = buffer.toString('base64');\n  }\n  originalName = item.json.name || 'drive_datei.pdf';\n  quelle = 'google_drive';\n}\n\nif (!fileBase64) {\n
         +  throw new Error('Keine Datei gefunden. Felder: ' + JSON.stringify(Object.keys(item.json)) + ', Binary-Keys: ' + JSON.stringify(Object.keys(item.binary || {})
         +));\n}\n\nreturn [{\n  json: {\n    file_base64: fileBase64,\n    mime_type: mimeType,\n    original_dateiname: originalName,\n    quelle: quelle,\n    eingang
         +_timestamp: new Date().toISOString()\n  }\n}];"         }
    },
    {
      "id": "aa000001-0000-0000-0000-000000000001",
      "name": "IF: Testmodus?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [680, 300],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond-testmodus",
              "leftValue": "={{ $json.quelle }}",
              "rightValue": "testmodus",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "aa000002-0000-0000-0000-000000000002",
      "name": "Code: Testdaten als OCR-Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [920, 160],
      "parameters": {
        "jsCode": "// Testmodus: Webhook-JSON direkt als OCR-Output mappen\nconst d = $input.first().json;\n\nreturn [{\n  json: {\n    rechnungsnummer:       d.rechnungsnummer       || null,\n    rechnungsdatum:        d.rechnungsdatum        || null,\n    leistungsdatum_von:    d.leistungsdatum_von    || null,\n    leistungsdatum_bis:    d.leistungsdatum_bis    || null,\n    faelligkeitsdatum:     d.faelligkeitsdatum     || null,\n    lieferant_name:        d.lieferant_name        || null,\n    lieferant_strasse:     d.lieferant_strasse     || null,\n    lieferant_plz:         d.lieferant_plz         || null,\n    lieferant_ort:         d.lieferant_ort         || null,\n    lieferant_land:        d.lieferant_land        || 'DE',\n    lieferant_ust_id:      d.lieferant_ust_id      || null,\n    netto:                 d.netto                 || null,\n    mwst_satz:             d.mwst_satz             || null,\n    mwst_betrag:           d.mwst_betrag           || null,\n    brutto:                d.brutto                || null,\n    waehrung:              d.waehrung              || 'EUR',\n    beschreibung:          d.beschreibung          || null,\n    extraktions_konfidenz: 1.0,\n    fehlende_felder:       [],\n    original_dateiname:    d.original_dateiname    || 'testmodus.pdf',\n    quelle:                'testmodus',\n    eingang_timestamp:     d.eingang_timestamp,\n    mistral_rohdaten:      null\n  }\n}];"
      }
    },
    {
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "name": "Code: Mistral Request aufbauen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [920, 440],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst fileBase64 = item.json.file_base64;\nconst mimeType = item.json.mime_type || 'application/pdf';\nconst isImage = mimeType.startsWith('image/');\n\n// Mistral OCR: images → image_url, PDFs → document_url\nconst document = isImage\n  ? { type: 'image_url', image_url: `data:${mimeType};base64,${fileBase64}` }\n  : { type: 'document_url', document_url: `data:application/pdf;base64,${fileBase64}` };\n\nconst body = {\n  model: 'mistral-ocr-latest',\n  document,\n  document_annotation_format: {\n    type: 'json_schema',\n    json_schema: {\n      name: 'deutsche_rechnung',\n      schema: {\n        type: 'object',\n        properties: {\n          rechnungsnummer:       { type: ['string', 'null'] },\n          rechnungsdatum:        { type: ['string', 'null'], description: 'Format: YYYY-MM-DD' },\n          leistungsdatum_von:    { type: ['string', 'null'], description: 'Format: YYYY-MM-DD' },\n          leistungsdatum_bis:    { type: ['string', 'null'], description: 'Format: YYYY-MM-DD' },\n          faelligkeitsdatum:     { type: ['string', 'null'], description: 'Format: YYYY-MM-DD' },\n          lieferant_name:        { type: ['string', 'null'] },\n          lieferant_strasse:     { type: ['string', 'null'] },\n          lieferant_plz:         { type: ['string', 'null'] },\n          lieferant_ort:         { type: ['string', 'null'] },\n          lieferant_land:        { type: ['string', 'null'], description: '2-stelliger ISO-Code, default: DE' },\n          lieferant_ust_id:      { type: ['string', 'null'] },\n          netto:                 { type: ['number', 'null'] },\n          mwst_satz:             { type: ['number', 'null'], description: '0, 7 oder 19' },\n          mwst_betrag:           { type: ['number', 'null'] },\n          brutto:                { type: ['number', 'null'] },\n          waehrung:              { type: 'string', default: 'EUR' },\n          beschreibung:          { type: ['string', 'null'], description: 'Kurze Leistungsbeschreibung' },\n          extraktions_konfidenz: { type: 'number', description: 'Zwischen 0 und 1' },\n          fehlende_felder:       { type: 'array', items: { type: 'string' } }\n        },\n        required: ['rechnungsdatum', 'lieferant_name', 'brutto', 'fehlende_felder']\n      }\n    }\n  }\n};\n\nreturn [{\n  json: {\n    original_dateiname: item.json.original_dateiname,\n    mime_type: mimeType,\n    quelle: item.json.quelle,\n    eingang_timestamp: item.json.eingang_timestamp,\n    mistral_body: JSON.stringify(body)\n  }\n}];\n"
      }
    },
    {
      "id": "d4e5f6a7-b8c9-0123-def0-234567890123",
      "name": "HTTP Request: Mistral OCR 3",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [1160, 440],
      "parameters": {
        "method": "POST",
        "url": "https://api.mistral.ai/v1/ocr",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ $json.mistral_body }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "1",
          "name": "Mistral OCR BuHa OptiMax Belegerkennung"
        }
      }
    },
    {
      "id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
      "name": "Code: OCR Response parsen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1400, 440],
      "parameters": {
        "jsCode": "// Produktionspfad: Mistral-Response auswerten\nconst mistralResponse = $input.first().json;\nconst extrahiert = mistralResponse.pages?.[0]?.annotations?.[0] || {};\n\n// Metadaten aus dem vorgelagerten Code-Node holen\nconst meta = $('Code: Mistral Request aufbauen').first().json;\n\nreturn [{\n  json: {\n    rechnungsnummer:       extrahiert.rechnungsnummer     || null,\n    rechnungsdatum:        extrahiert.rechnungsdatum      || null,\n    leistungsdatum_von:    extrahiert.leistungsdatum_von  || null,\n    leistungsdatum_bis:    extrahiert.leistungsdatum_bis  || null,\n    faelligkeitsdatum:     extrahiert.faelligkeitsdatum   || null,\n    lieferant_name:        extrahiert.lieferant_name      || null,\n    lieferant_strasse:     extrahiert.lieferant_strasse   || null,\n    lieferant_plz:         extrahiert.lieferant_plz       || null,\n    lieferant_ort:         extrahiert.lieferant_ort       || null,\n    lieferant_land:        extrahiert.lieferant_land      || 'DE',\n    lieferant_ust_id:      extrahiert.lieferant_ust_id    || null,\n    netto:                 extrahiert.netto               || null,\n    mwst_satz:             extrahiert.mwst_satz           || null,\n    mwst_betrag:           extrahiert.mwst_betrag         || null,\n    brutto:                extrahiert.brutto              || null,\n    waehrung:              extrahiert.waehrung            || 'EUR',\n    beschreibung:          extrahiert.beschreibung        || null,\n    extraktions_konfidenz: extrahiert.extraktions_konfidenz || 0,\n    fehlende_felder:       extrahiert.fehlende_felder     || [],\n    original_dateiname:    meta.original_dateiname,\n    quelle:                meta.quelle,\n    eingang_timestamp:     meta.eingang_timestamp,\n    mistral_rohdaten:      mistralResponse\n  }\n}];"
      }
    },
    {
      "id": "f6a7b8c9-d0e1-2345-f012-456789012345",
      "name": "Code: Validierung",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1640, 300],
      "parameters": {
        "jsCode": "const daten = $input.first().json;\nconst fehler = [];\nconst warnungen = [];\n\n// Pflichtfelder prüfen\nif (!daten.rechnungsdatum)               fehler.push('Rechnungsdatum fehlt');\nif (!daten.lieferant_name)               fehler.push('Lieferantenname fehlt');\nif (!daten.brutto || daten.brutto <= 0)  fehler.push('Bruttobetrag fehlt oder ungültig');\n\n// Plausibilität: Netto + MwSt = Brutto?\nif (daten.netto && daten.mwst_betrag && daten.brutto) {\n  const berechnet = Math.round((daten.netto + daten.mwst_betrag) * 100) / 100;\n  const differenz = Math.abs(berechnet - daten.brutto);\n  if (differenz > 0.05) {\n    warnungen.push(`Summenprüfung: ${daten.netto} + ${daten.mwst_betrag} = ${berechnet}, aber Brutto ist ${daten.brutto}`);\n  }\n}\n\n// Fehlende Netto-Berechnung ergänzen\nif (daten.brutto && daten.mwst_satz && !daten.netto) {\n  daten.netto = Math.round((daten.brutto / (1 + daten.mwst_satz / 100)) * 100) / 100;\n  daten.mwst_betrag = Math.round((daten.brutto - daten.netto) * 100) / 100;\n  warnungen.push('Netto und MwSt-Betrag wurden berechnet (nicht direkt aus Rechnung gelesen)');\n}\n\n// Datum-Plausibilität\nif (daten.rechnungsdatum) {\n  const rDatum = new Date(daten.rechnungsdatum);\n  const heute = new Date();\n  const zweiJahreAlt = new Date();\n  zweiJahreAlt.setFullYear(heute.getFullYear() - 2);\n  if (rDatum > heute)        warnungen.push('Rechnungsdatum liegt in der Zukunft');\n  if (rDatum < zweiJahreAlt) warnungen.push('Rechnungsdatum ist über 2 Jahre alt');\n}\n\nreturn [{\n  json: {\n    ...daten,\n    validierung: {\n      valid: fehler.length === 0,\n      fehler: fehler,\n      warnungen: warnungen,\n      geprueft_am: new Date().toISOString()\n    }\n  }\n}];"
      }
    },
    {
      "id": "a7b8c9d0-e1f2-3456-0123-567890123456",
      "name": "Validierung OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1880, 300],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond-1",
              "leftValue": "={{ $json.validierung.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "b8c9d0e1-f2a3-4567-1234-678901234567",
      "name": "Respond: Erfolg",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [2120, 180],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseCode": 200
        }
      }
    },
    {
      "id": "c9d0e1f2-a3b4-5678-2345-789012345678",
      "name": "Respond: Fehler",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [2120, 420],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ fehler: $json.validierung.fehler, warnungen: $json.validierung.warnungen, lieferant: $json.lieferant_name, rechnungsdatum: $json.rechnungsdatum }) }}",
        "options": {
          "responseCode": 422
        }
      }
    }
  ],
  "connections": {
    "Webhook: Beleg Upload": {
      "main": [
        [{ "node": "Code: Normalisierung", "type": "main", "index": 0 }]
      ]
    },
    "Code: Normalisierung": {
      "main": [
        [{ "node": "IF: Testmodus?", "type": "main", "index": 0 }]
      ]
    },
    "IF: Testmodus?": {
      "main": [
        [{ "node": "Code: Testdaten als OCR-Output", "type": "main", "index": 0 }],
        [{ "node": "Code: Mistral Request aufbauen", "type": "main", "index": 0 }]
      ]
    },
    "Code: Testdaten als OCR-Output": {
      "main": [
        [{ "node": "Code: Validierung", "type": "main", "index": 0 }]
      ]
    },
    "Code: Mistral Request aufbauen": {
      "main": [
        [{ "node": "HTTP Request: Mistral OCR 3", "type": "main", "index": 0 }]
      ]
    },
    "HTTP Request: Mistral OCR 3": {
      "main": [
        [{ "node": "Code: OCR Response parsen", "type": "main", "index": 0 }]
      ]
    },
    "Code: OCR Response parsen": {
      "main": [
        [{ "node": "Code: Validierung", "type": "main", "index": 0 }]
      ]
    },
    "Code: Validierung": {
      "main": [
        [{ "node": "Validierung OK?", "type": "main", "index": 0 }]
      ]
    },
    "Validierung OK?": {
      "main": [
        [{ "node": "Respond: Erfolg", "type": "main", "index": 0 }],
        [{ "node": "Respond: Fehler", "type": "main", "index": 0 }]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "instanceId": "buha-optimax-belegerkennung"
  },
  "id": "workflow-01-eingang-ocr",
  "tags": [
    { "name": "buha-optimax" },
    { "name": "sprint-1-2" }
  ]
}
