{
  "name": "PDF Invoices from Mailbox → ZUGFeRD / XRechnung (per department)",
  "nodes": [
    {
      "parameters": {
        "mailbox": "INBOX",
        "postProcessAction": "read",
        "downloadAttachments": true,
        "format": "resolved",
        "dataPropertyAttachmentsPrefixName": "attachment_",
        "options": {
          "customEmailConfig": "[\"UNSEEN\"]",
          "forceReconnect": 60
        }
      },
      "name": "Mailbox: Musterfirma (IMAP)",
      "type": "n8n-nodes-base.emailReadImap",
      "typeVersion": 2,
      "position": [
        -1000,
        300
      ],
      "credentials": {},
      "id": "eadf1613-2cb4-475c-a7c3-533a5562b32c"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dep-1",
              "name": "department",
              "value": "musterfirma",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "name": "Department: musterfirma",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -780,
        300
      ],
      "id": "e23f9d47-8886-44c8-9d3c-b464c29b8d52"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ── Load Master Data ──────────────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n//\n// The only node you maintain in day-to-day operation. It holds two tables:\n//\n//   DEPARTMENTS — per department: mailbox, parser, template, sender details,\n//                 output paths and the address that receives error reports\n//   CUSTOMERS   — per buyer: e-mail address and Leitweg-ID / buyer reference.\n//                 Neither is printed on the incoming PDFs, so both must come\n//                 from here.\n//\n// Which department applies is set by the small Set node behind each IMAP\n// trigger (field `department`).\n\nconst DEPARTMENTS = {\n  musterfirma: {\n    name: 'Musterfirma GmbH',\n    rules: null,                       // null = preset \"Generic Invoice (DE)\" in node \"Extract Fields\";\n                                       // or a rule set object (docs/RULES.md of n8n-nodes-pdf2invoice-api-xhub)\n    template: 'generic',               // see node \"Select PDF Template\"\n    pageSeparator: null,               // header line printed on every page; only needed if the\n                                       // Extract node ever returns one string instead of pages\n    mailbox: 'invoices@example.com',\n    errorMail: 'office@example.com',   // gets the plain-language rejection mail\n    releaseMail: '',                   // optional: who approves documents in node \"Request Release\" (empty = errorMail)\n    senderMail: 'invoices@example.com',\n    outputPath: '/home/node/e-invoice/out/musterfirma',\n    archivePath: '/home/node/e-invoice/in/musterfirma',\n    logPath: '/home/node/e-invoice/log/musterfirma.jsonl',\n    targetFormat: 'zugferd',           // zugferd | facturx | xrechnung | ubl | cii\n    templateId: '',                    // set = the template stored in your invoice-api.xhub account wins\n    seller: {\n      name: 'Musterfirma GmbH',\n      street: 'Musterstraße 1', postalCode: '12345', city: 'Musterstadt', countryCode: 'DE',\n      vatId: 'DE123456788',\n      email: 'invoices@example.com',\n      // BR-DE-2 requires a seller contact, BR-DE-6 (XRechnung) its phone number\n      contact: { name: 'Erika Muster', phone: '+49 30 1234567', email: 'erika.muster@example.com' },\n      // BR-DE-1 requires payment details — IBAN below is the official test IBAN\n      bankAccount: { iban: 'DE02120300000000202051', bic: 'BYLADEM1001', bankName: 'Deutsche Kreditbank' },\n    },\n    // Anything printed on the letterhead that EN 16931 has no field for.\n    // Only used by the PDF template, never written into the XML.\n    extras: {\n      registerCourt: 'Amtsgericht Musterstadt',\n      registerNumber: 'HRB 12345',\n      managingDirector: 'Max Muster',\n    },\n  },\n};\n\n// ── Customer master data ──────────────────────────────────────────────\n// Key = buyer name as printed in the address block, lower-cased, whitespace\n// collapsed. If your documents carry a customer number, key by that instead.\nconst CUSTOMERS = {\n  'beispiel kunde ag': {\n    name: 'Beispiel Kunde AG',\n    email: 'accounts-payable@example.org',\n    leitwegId: '991-12345-67',        // buyer reference (BT-10); Leitweg-ID for B2G\n    // street/postalCode/city/countryCode/vatId are optional — if set they win over the PDF\n    // peppolId: '0204:991-12345-67', // only when you send via Peppol\n  },\n};\n\n/** buyer name → master data key (lower-case, single spaces) */\nfunction customerKey(name) {\n  return String(name || '').toLowerCase().replace(/\\s+/g, ' ').trim();\n}\n\nconst key = String($input.item.json.department || '').toLowerCase();\nconst department = DEPARTMENTS[key];\n\nif (!department) {\n  throw new Error(\n    `Unknown department \"${key}\". Known keys: ${Object.keys(DEPARTMENTS).join(', ')}. ` +\n      'The Set node behind the IMAP trigger must set one of these.'\n  );\n}\n\nreturn {\n  json: {\n    ...$input.item.json,\n    departmentKey: key,\n    department,\n    customers: CUSTOMERS,\n    customerKeyRule: 'lower-case, whitespace collapsed',\n  },\n  binary: $input.item.binary,\n};\n"
      },
      "name": "Load Master Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -560,
        300
      ],
      "id": "e44257bb-460a-4de3-badb-0803fad760a1"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// ── Split PDF Attachments ─────────────────────────────────────────────\n// Mode: \"Run Once for All Items\"\n// Takes the mails from the IMAP trigger and emits one item per PDF attachment.\n// Mails without a PDF drop out silently.\n//\n// The department's master data (from the previous node) travels with every\n// item so that the following nodes never have to look back.\n\n/** \"Max Muster <max@example.com>\" → \"max@example.com\" */\nfunction mailAddress(value) {\n  if (!value) return null;\n  if (typeof value === 'object') {\n    if (Array.isArray(value.value) && value.value[0]?.address) return value.value[0].address;\n    if (value.address) return value.address;\n    if (value.text) return mailAddress(value.text);\n    return null;\n  }\n  const m = String(value).match(/<([^>]+)>/);\n  return (m ? m[1] : String(value)).trim().toLowerCase() || null;\n}\n\n/** display name, if any */\nfunction mailName(value) {\n  if (!value) return null;\n  if (typeof value === 'object') {\n    if (Array.isArray(value.value) && value.value[0]?.name) return value.value[0].name;\n    if (value.name) return value.name;\n    if (value.text) return mailName(value.text);\n    return null;\n  }\n  const m = String(value).match(/^\\s*\"?([^\"<]+?)\"?\\s*</);\n  return m ? m[1].trim() : null;\n}\n\nconst out = [];\n\n$input.all().forEach((item, mailIndex) => {\n  const mail = item.json || {};\n  // carry only the master-data fields, not the whole raw mail record\n  const base = {\n    departmentKey: mail.departmentKey,\n    department: mail.department,\n    customers: mail.customers,\n  };\n  const binary = item.binary || {};\n\n  const messageId = String(mail.messageId || mail.id || `mail-${mailIndex}`).replace(/[<>]/g, '');\n\n  const mailMeta = {\n    messageId,\n    from: mailAddress(mail.from),\n    fromName: mailName(mail.from),\n    to: mailAddress(mail.to),\n    subject: mail.subject || null,\n    date: mail.date || null,\n  };\n\n  let n = 0;\n  for (const [key, bin] of Object.entries(binary)) {\n    if (!bin) continue;\n    const name = bin.fileName || '';\n    const isPdf = bin.mimeType === 'application/pdf' || /\\.pdf$/i.test(name);\n    if (!isPdf) continue;\n\n    out.push({\n      json: {\n        ...base,\n        docId: `${messageId}#${n}`,\n        mail: mailMeta,\n        sourceFile: name || `attachment-${n}.pdf`,\n        sourceField: key,\n        receivedAt: new Date().toISOString(),\n      },\n      binary: { pdf: bin },\n      pairedItem: { item: mailIndex },\n    });\n    n++;\n  }\n});\n\n// empty array = no PDFs in this mail, the branch ends here\nreturn out;\n"
      },
      "name": "Split PDF Attachments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -340,
        300
      ],
      "id": "c51ea952-9add-4029-af85-c0b6725f13d6"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.department.archivePath }}/{{ $now.format('yyyy-MM-dd') }}_{{ $json.sourceFile }}",
        "dataPropertyName": "pdf",
        "options": {}
      },
      "name": "Archive Source PDF",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        -120,
        120
      ],
      "onError": "continueRegularOutput",
      "id": "30159b7a-ab1f-497a-9a36-a988031befdf"
    },
    {
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "pdf",
        "options": {
          "joinPages": false,
          "keepSource": "both"
        }
      },
      "name": "Extract from File (PDF)",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -120,
        400
      ],
      "id": "4fd5c247-99be-4495-8958-3ee4499d9f5c"
    },
    {
      "parameters": {
        "operation": "validate",
        "countryCode": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.department.seller.countryCode }}"
        },
        "invoiceData": "={{ JSON.stringify($json.invoice) }}",
        "options": {
          "failOnErrors": false,
          "failOnWarnings": false
        }
      },
      "name": "invoice-api.xhub: Validate",
      "type": "n8n-nodes-invoice-api-xhub.invoiceXhub",
      "typeVersion": 1,
      "position": [
        760,
        400
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "credentials": {},
      "id": "16e2e003-3d60-46a0-b0a9-28688f9fcdcd"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "own-check",
              "leftValue": "={{ $('Assemble Invoice').item.json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            },
            {
              "id": "api-valid",
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            },
            {
              "id": "api-formats",
              "leftValue": "={{ ($json.results ?? []).filter(r => String(r.format).startsWith(String($('Assemble Invoice').item.json.department.targetFormat).toLowerCase())).every(r => r.valid !== false) }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        980,
        400
      ],
      "id": "e9f63201-79f2-4f8a-b5af-e0be80aadbf5"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ── Select PDF Template ───────────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n//\n// Every department can have its own visual layout. Which one applies is set\n// in master data (`department.template`). The template is built PER\n// DOCUMENT, so anything the template language cannot do — long-form dates,\n// country names, letterhead extras — is rendered here as finished text.\n// The XML still carries the normalised values.\n//\n// If `department.templateId` is set, the template stored in your\n// invoice-api.xhub account wins and this node only passes the ID on.\n//\n// Units: this template declares `lengthUnit: 'pt'`, so every spatial value\n// (margins, widths, heights, gaps) is in points. Do not omit `lengthUnit` —\n// a template without it is read in legacy mode, where page margins and\n// spacer heights are millimetres while everything else is points.\n// `locale` is set on every summary row: that is where number formatting is\n// read (de-DE → 18.391,71 instead of 18,391.71).\n\n// Upstream nodes replaced the item JSON (Validate, Fingerprint, Check Duplicate),\n// so the assembled invoice and the validation verdict are fetched by paired item.\nconst built = $('Assemble Invoice').item.json;\nconst validation = $('invoice-api.xhub: Validate').item.json;\nconst input = { ...built, validation: { valid: validation.valid, warnings: validation.warnings ?? [], results: validation.results ?? [] } };\nconst dep = input.department || {};\nconst d = input.invoice || {};\nconst kind = String(dep.template || 'generic').toLowerCase();\n\nif (dep.templateId) {\n  return { json: { ...input, templateId: dep.templateId, formatOptions: {}, templateSource: 'account' },\n           binary: $input.item.binary };\n}\n\nconst txt = (content, k = {}) => ({ type: 'text', data: { content, ...k } });\nconst COUNTRY = { DE: 'Deutschland', AT: 'Österreich', CH: 'Schweiz', NL: 'Niederlande',\n                  FR: 'Frankreich', BE: 'Belgien' };\nconst countryName = (code) => COUNTRY[String(code || '').toUpperCase()] || String(code || '');\nconst shortDate = (iso) => { const m = String(iso || '').match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n  return m ? `${m[3]}.${m[2]}.${m[1]}` : String(iso || ''); };\n\nconst LOCALE = 'de-DE';\nconst money = { format: 'currency', decimalPlaces: 2, locale: LOCALE };\n\nfunction templateGeneric(x, d, isCreditNote) {\n  const F = { small: 7.5, text: 10, title: 14 };\n  const title = isCreditNote ? 'Gutschrift' : 'Rechnung';\n\n  return {\n    version: '1.0.0',\n    name: 'generic-letter',\n    lengthUnit: 'pt',\n    page: { size: 'A4', orientation: 'portrait',\n            margins: { top: 57, right: 45, bottom: 57, left: 60 } },\n    footer: { height: 60, blocks: [\n      txt(`{{seller.name}} · {{seller.street}} · {{seller.postalCode}} {{seller.city}} · USt-IdNr. {{seller.vatId}}`,\n        { fontSize: F.small, alignment: 'center' }),\n      txt(`${x.registerCourt ? `${x.registerCourt} ${x.registerNumber} · ` : ''}` +\n          `${x.managingDirector ? `Geschäftsführung: ${x.managingDirector} · ` : ''}` +\n          `IBAN {{seller.bankAccount.iban}} · BIC {{seller.bankAccount.bic}}`,\n        { fontSize: F.small, alignment: 'center' }),\n    ]},\n    body: { blocks: [\n      txt('{{seller.name}} • {{seller.street}} • {{seller.postalCode}} {{seller.city}}',\n        { fontSize: F.small, margin: [0, 40, 0, 12] }),\n      { type: 'columns', data: { columnGap: 20, columns: [\n        { width: '*', blocks: [\n          txt('{{buyer.name}}', { fontSize: F.text }),\n          txt('{{buyer.street}}', { fontSize: F.text }),\n          txt('{{buyer.postalCode}} {{buyer.city}}', { fontSize: F.text }),\n          txt(countryName(d.buyer && d.buyer.countryCode), { fontSize: F.text, margin: [0, 0, 0, 30] }),\n        ]},\n        { width: 200, blocks: [{ type: 'keyvalue', data: {\n          layout: 'horizontal', fontSize: F.text, labelBold: true, widths: [100, 100],\n          items: [\n            { label: `${title}snummer`, value: '{{invoiceNumber}}' },\n            { label: 'Datum', value: shortDate(d.issueDate) },\n            ...(isCreditNote ? [] : [{ label: 'Fällig am', value: shortDate(d.dueDate) }]),\n            ...(d.orderNumber ? [{ label: 'Ihre Bestellung', value: '{{orderNumber}}' }] : []),\n            ...(d.countrySpecific?.buyerReference ? [{ label: 'Leitweg-ID', value: '{{countrySpecific.buyerReference}}' }] : []),\n          ],\n        }}]},\n      ]}},\n      txt(`${title} {{invoiceNumber}}`, { fontSize: F.title, bold: true, margin: [0, 10, 0, 14] }),\n      txt('{{notes?}}', { fontSize: F.text, margin: [0, 0, 0, 12] }),\n      { type: 'table', data: {\n        dataSource: '{{items}}', layout: 'lightHorizontalLines', showHeader: true,\n        fontSize: F.text, locale: LOCALE,\n        cellPadding: { top: 4, bottom: 4, left: 0, right: 4 },\n        columns: [\n          { field: 'position', header: 'Pos.', width: 30 },\n          { field: 'description', header: 'Bezeichnung', width: '*' },\n          { field: 'quantity', header: 'Menge', width: 45, align: 'right', format: 'number', decimalPlaces: 0, locale: LOCALE },\n          { field: 'unitPrice', header: 'Einzelpreis', width: 75, align: 'right', ...money },\n          { field: 'netAmount', header: 'Netto', width: 80, align: 'right', ...money },\n        ],\n      }},\n      { type: 'summary', data: {\n        widths: [160, 100], fontSize: F.text,\n        headerRows: [],                                              // required by the schema, even when empty\n        dynamicRows: { dataSource: '{{taxSummary}}', labelTemplate: 'zzgl. {{taxRate}} % USt.',\n                       valueField: 'taxAmount', valueFormat: 'currency', locale: LOCALE },\n        footerRows: [\n          { label: 'Nettobetrag', value: '{{subtotal}}', valueFormat: 'currency', locale: LOCALE },\n          { label: `${title}sbetrag`, value: '{{total}}', bold: true, valueFormat: 'currency', locale: LOCALE },\n        ],\n        separatorBeforeFooter: true,\n      }},\n      txt('{{paymentTerms.description}}', { fontSize: F.text, margin: [0, 20, 0, 0] }),\n    ]},\n  };\n}\n\nconst TEMPLATES = { generic: templateGeneric };\nconst build = TEMPLATES[kind];\nif (!build) {\n  throw new Error(`No template \"${kind}\" defined. Available: ${Object.keys(TEMPLATES).join(', ')}.`);\n}\n\nconst template = build(dep.extras || {}, d, Boolean((d.type === 'credit_note')));\n\nreturn {\n  json: { ...input, templateId: '', templateName: template.name, templateSource: 'workflow',\n          formatOptions: { template } },\n  binary: $input.item.binary,\n};\n"
      },
      "name": "Select PDF Template",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2520,
        240
      ],
      "id": "72953695-7a39-4fa4-a941-f2b6c5729b4c"
    },
    {
      "parameters": {
        "operation": "generate",
        "countryCode": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.department.seller.countryCode }}"
        },
        "format": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.department.targetFormat }}"
        },
        "invoiceData": "={{ JSON.stringify($json.invoice) }}",
        "options": {
          "outputBinary": true,
          "binaryPropertyName": "data",
          "includeWarnings": true,
          "templateId": "={{ $json.templateId }}",
          "formatOptions": "={{ JSON.stringify($json.formatOptions) }}"
        }
      },
      "name": "invoice-api.xhub: Generate",
      "type": "n8n-nodes-invoice-api-xhub.invoiceXhub",
      "typeVersion": 1,
      "position": [
        2740,
        240
      ],
      "onError": "continueErrorOutput",
      "credentials": {},
      "id": "2d13c45c-9305-416e-8ac7-f03cdde12b17"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "gen-ok",
              "leftValue": "={{ $json.success }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Generated?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2960,
        240
      ],
      "id": "21c6b3f0-7c6e-475b-b14c-7b5329bb1346"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ── Prepare Result ────────────────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n// Decides file name, output path, recipient and cover mail. The generated\n// document stays attached as binary \"data\".\n\nconst gen = $input.item.json;\nconst doc = $('Assemble Invoice').item.json;\nconst dep = doc.department || {};\nconst d = doc.invoice;\n\n// File name and content fingerprint come from the **Fingerprint** node\n// (payloadHash = SHA-256 over the canonical invoice JSON — the same value the\n// API returns as `payloadHash`). Not the API's `hash`: that is the SHA-256 of\n// the generated bytes and changes on every call (PDF timestamp).\nconst fp = $('Fingerprint').item.json;\nconst fingerprint = fp.payloadHash;\nconst fileName = fp.fileName;\nconst ext = fp.extension;\n\nconst kind = d.type === 'credit_note' ? 'Credit note' : 'Invoice';\n\nconst targetPath = `${String(dep.outputPath || '.').replace(/\\/+$/, '')}/${fileName}`;\n\n// Recipient: address from the customer table. If it is missing, the document\n// already stopped in the check — this branch is never reached then.\nconst recipient = String(doc.recipient || d.buyer?.email || '').trim().toLowerCase();\nif (!recipient) {\n  throw new Error(\n    `No recipient for ${d.invoiceNumber}: \"${d.buyer?.name}\" has no e-mail address in the customer table.`);\n}\n\nconst amount = `${new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2 }).format(Number(d.total))} ${d.currency}`;\nconst FORMAT_LABEL = { zugferd: 'ZUGFeRD', facturx: 'Factur-X', xrechnung: 'XRechnung',\n                       ubl: 'UBL', 'peppol-ubl': 'Peppol UBL', cii: 'CII' };\nconst formatLabel = FORMAT_LABEL[String(gen.format).toLowerCase()] || String(gen.format);\n\n// Cover mail — adjust wording and language to your buyers.\nconst mailText = [\n  'Dear Sir or Madam,',\n  '',\n  `please find attached ${kind.toLowerCase()} ${d.invoiceNumber} dated ${d.issueDate} for ${amount} as ${formatLabel}.`,\n  d.type === 'credit_note' ? null : `Due by ${d.dueDate}.`,\n  '',\n  'The attached document conforms to EN 16931 and can be imported directly into your accounting system.',\n  '',\n  'Kind regards',\n  d.seller.name,\n].filter((l) => l !== null).join('\\n');\n\nreturn {\n  json: {\n    docId: doc.docId,\n    status: 'generated',\n    department: doc.departmentKey,\n\n    invoiceNumber: d.invoiceNumber,\n    documentType: d.type,\n    issueDate: d.issueDate,\n    dueDate: d.dueDate ?? null,\n    netAmount: d.subtotal,\n    grossAmount: d.total,\n    currency: d.currency,\n    seller: d.seller.name,\n    buyer: d.buyer.name,\n    buyerReference: d.countrySpecific?.buyerReference ?? null,\n\n    format: gen.format,\n    warnings: gen.warnings ?? [],\n    hasEmbeddedXml: Boolean(gen.embeddedXml),\n    template: $('Select PDF Template').item.json.templateName ?? null,\n\n    fingerprint,\n    payloadHash: fp.payloadHash,\n    shortHash: fp.shortHash,\n    fileName,\n    targetPath,\n    logPath: dep.logPath,\n    recipient,\n    sender: dep.senderMail,\n    departmentErrorMail: dep.errorMail,\n    subject: `${kind} ${d.invoiceNumber} – ${d.seller.name}`,\n    mailText,\n\n    sourceFile: doc.sourceFile,\n    sourceMail: doc.mail,\n  },\n  binary: $input.item.binary,\n};\n"
      },
      "name": "Prepare Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3180,
        240
      ],
      "id": "36e5849e-479e-4e4f-83d1-f348889211d5"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.targetPath }}",
        "dataPropertyName": "data",
        "options": {}
      },
      "name": "Store E-Invoice",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        3400,
        240
      ],
      "id": "8e3b06fe-668a-478c-a0c2-2ec95eec2a2f"
    },
    {
      "parameters": {
        "fromEmail": "={{ $json.sender }}",
        "toEmail": "={{ $json.recipient }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.mailText }}",
        "options": {
          "appendAttribution": false,
          "fileAttachments": "data"
        }
      },
      "name": "Send to Buyer",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        3620,
        240
      ],
      "credentials": {},
      "id": "5d8cfa32-cca1-4331-8e84-c3e9d0ed09b5"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ── Build Log Line ────────────────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n// One line in JSON Lines format, as binary, so the write node can append it\n// to the department's log file.\n\n// Send to Buyer replaces the item JSON with the SMTP result (accepted, messageId, …),\n// so the document data comes from the node that decided it is new (paired item).\nconst e = $('Prepare Result').item.json;\nconst sent = $input.item.json;\n\nconst entry = {\n  timestamp: new Date().toISOString(),\n  execution: $execution.id,\n  workflow: $workflow.name,\n  status: 'ok',\n\n  department: e.department,\n  docId: e.docId,\n  fingerprint: e.fingerprint,\n  payloadHash: e.payloadHash,\n  sourceFile: e.sourceFile,\n  sourceMailFrom: e.sourceMail?.from ?? null,\n  sourceMailSubject: e.sourceMail?.subject ?? null,\n\n  invoiceNumber: e.invoiceNumber,\n  documentType: e.documentType,\n  issueDate: e.issueDate,\n  dueDate: e.dueDate,\n  netAmount: e.netAmount,\n  grossAmount: e.grossAmount,\n  currency: e.currency,\n  seller: e.seller,\n  buyer: e.buyer,\n  buyerReference: e.buyerReference,\n\n  format: e.format,\n  template: e.template,\n  warnings: e.warnings,\n  storedAt: e.targetPath,\n  sentTo: e.recipient,\n  sentMessageId: sent?.messageId ?? null,\n};\n\nreturn {\n  json: { ...entry, logPath: e.logPath },\n  binary: {\n    logLine: {\n      data: Buffer.from(JSON.stringify(entry) + '\\n', 'utf8').toString('base64'),\n      mimeType: 'application/x-ndjson',\n      fileName: 'e-invoice.jsonl',\n    },\n  },\n};\n"
      },
      "name": "Build Log Line",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3840,
        240
      ],
      "id": "ec6b4b4b-e73f-4c6f-9fa5-4c50923e6f52"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $('Prepare Result').item.json.logPath }}",
        "dataPropertyName": "logLine",
        "options": {
          "append": true
        }
      },
      "name": "Append Log",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        4060,
        240
      ],
      "id": "98b2363c-6d2d-4baa-8324-cdee272d5b2e"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ── Prepare Error ─────────────────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n//\n// The document failed the in-workflow check, the API validation or the\n// generation. Nothing is sent to the buyer. Instead: a log line and a mail\n// back to the department that sent the document — they can fix it, the\n// buyer never sees any of this.\n\nconst response = $input.item.json;\nconst doc = $('Assemble Invoice').item.json;\nconst dep = doc.department || {};\nconst d = doc.invoice || {};\n\n// `checks[]` from Assemble Invoice: errors stop the document, warnings ride along\nconst ownCheck = (doc.checks ?? []).filter((c) => c.severity === 'error')\n  .map((c) => `[${c.code}] ${c.message}${c.field ? ` (${c.field})` : ''}`);\n\nconst asText = (f) => {\n  const msg = f.message ?? JSON.stringify(f);\n  const code = f.code && !String(msg).startsWith(`[${f.code}]`) ? `[${f.code}] ` : '';\n  return `${code}${msg}${f.field ? ` (${f.field})` : ''}`;\n};\n\n// `errors` come from validation, `complianceErrors` from generation (Schematron\n// check of XRechnung/ZUGFeRD), `results[]` carries the per-format XSD+Schematron\n// verdict of /validate — collect all of them.\nconst apiErrors = [\n  ...(response.errors ?? []).map(asText),\n  ...(response.complianceErrors ?? []).map(asText),\n  ...(response.results ?? []).flatMap((r) => (r.valid === false ? (r.errors ?? []).map((f) => `[${r.format}] ${asText(f)}`) : [])),\n];\nif (response.error) apiErrors.push(typeof response.error === 'object' ? (response.error.message ?? JSON.stringify(response.error)) : String(response.error));\nif (response.data && response.data.approved === false) {\n  apiErrors.push(`Release refused by the reviewer${response.data.respondedAt ? ` (${response.data.respondedAt})` : ''} — nothing was generated or sent.`);\n}\nconst apiWarnings = (response.warnings ?? []).map((w) => w.message ?? JSON.stringify(w));\n\n// warnings from Extract Fields (e.g. \"No document recognised …\") explain an empty document\nconst readWarnings = Array.isArray(doc.warnings) ? doc.warnings.map(String) : [];\nconst allMessages = [...readWarnings, ...ownCheck, ...apiErrors];\n\nconst entry = {\n  timestamp: new Date().toISOString(),\n  execution: $execution.id,\n  workflow: $workflow.name,\n  status: 'error',\n\n  department: doc.departmentKey,\n  docId: doc.docId,\n  sourceFile: doc.sourceFile,\n  sourceMailFrom: doc.mail?.from ?? null,\n  sourceMailSubject: doc.mail?.subject ?? null,\n\n  invoiceNumber: d.invoiceNumber ?? null,\n  documentType: d.type ?? null,\n  grossAmount: d.total ?? null,\n  seller: d.seller?.name ?? null,\n  buyer: d.buyer?.name ?? null,\n\n  ownCheck,\n  apiErrors,\n  apiWarnings,\n};\n\nconst mailText = [\n  'a document from your mailbox could not be turned into an e-invoice.',\n  '',\n  `Department:   ${doc.departmentKey} (${dep.name ?? '–'})`,\n  `Source file:  ${doc.sourceFile}`,\n  `From mail by: ${doc.mail?.from ?? 'unknown'}`,\n  `Subject:      ${doc.mail?.subject ?? '–'}`,\n  `Document:     ${d.invoiceNumber ?? 'no number'}` +\n    ((doc.documentsFound ?? 1) > 1 ? ` (document ${(doc.document?.index ?? 0) + 1} of ${(doc.documentsFound ?? 1)}, page ${(doc.document?.pageRange ?? null)})` : ''),\n  `Execution:    ${$execution.id}`,\n  '',\n  'Problems found:',\n  ...allMessages.map((m) => `  · ${m}`),\n  ...(apiWarnings.length ? ['', 'Warnings:', ...apiWarnings.map((m) => `  · ${m}`)] : []),\n  '',\n  'Nothing was sent to the buyer. The source PDF is stored unchanged under',\n  `${dep.archivePath ?? '–'}.`,\n  'After correcting it, simply send the mail to this mailbox again.',\n].join('\\n');\n\nreturn {\n  json: {\n    ...entry,\n    logPath: dep.logPath,\n    recipient: dep.errorMail,\n    sender: dep.senderMail,\n    subject: `[E-Invoice] Document rejected: ${d.invoiceNumber ?? doc.sourceFile}`,\n    mailText,\n    messageCount: allMessages.length,\n  },\n  binary: {\n    logLine: {\n      data: Buffer.from(JSON.stringify(entry) + '\\n', 'utf8').toString('base64'),\n      mimeType: 'application/x-ndjson',\n      fileName: 'e-invoice.jsonl',\n    },\n  },\n};\n"
      },
      "name": "Prepare Error",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        660
      ],
      "id": "ddafc71e-0e3f-45e1-9251-4981d5770254"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.logPath }}",
        "dataPropertyName": "logLine",
        "options": {
          "append": true
        }
      },
      "name": "Append Error Log",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1420,
        660
      ],
      "id": "40bcdcbf-5a91-4b09-8afc-6ab13d1d0f13"
    },
    {
      "parameters": {
        "fromEmail": "={{ $json.sender }}",
        "toEmail": "={{ $json.recipient }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.mailText }}",
        "options": {
          "appendAttribution": false
        }
      },
      "name": "Notify Department",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1640,
        660
      ],
      "credentials": {},
      "id": "4c56867c-02dd-4050-85c1-83f389ef158d"
    },
    {
      "parameters": {
        "content": "## Mailbox → PDF → ZUGFeRD / XRechnung\n\nEach department mails its **PDF invoices** (from Word or Excel) to its own mailbox. This workflow reads the PDF with a **rule set** (no LLM), rebuilds the invoice as EN 16931 data, lets `invoice-api.xhub` validate and generate the e-invoice, sends it to the buyer and writes one log line per document.\n\nBuilt in: a duplicate guard across runs, **before** anything is generated (note 6). Optional: a four-eyes release by mail (node **Request Release**, disabled by default).\n\n**Before you activate** — fill the two tables in **Load Master Data** (your department + your customers), connect IMAP, SMTP and invoice-api.xhub, create the output directories (note 6) and send the example PDF to the mailbox once.\n\nRequires **two** community nodes: `n8n-nodes-invoice-api-xhub` ≥ 1.1.3 (Validate, Generate) and `n8n-nodes-pdf2invoice-api-xhub` (Normalize Text, Extract Fields, Assemble Invoice, Fingerprint, Check Duplicate — local, no credentials). `n8n-nodes-pdf2invoice-api-xhub` is **not published on npm yet** (checked 2026-09-15), so \"Community Nodes \u2192 Install\" cannot find it and this import fails with \"unknown node\" until it is: source at github.com/xhubio/n8n-nodes-pdf2invoice-api-xhub, build it and drop it into `~/.n8n/nodes` on a self-hosted instance.",
        "height": 490,
        "width": 620,
        "color": 4
      },
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1060,
        -820
      ],
      "id": "1e31e89a-907d-4f9a-80b5-a9a439aa449a"
    },
    {
      "parameters": {
        "content": "## 1 · One mailbox per department\nEvery department sends its invoices to its own address. The Set node behind the trigger sets exactly one field: `department`. Everything else comes from master data.\n\n**Adding a department — four steps:**\n1. Copy the IMAP trigger, enter the new mailbox credentials\n2. Copy the Set node, set `department` to the new key\n3. Add an entry under `DEPARTMENTS` in **Load Master Data**\n4. Add a parser for the department's layout in **Read Document**\n\nAll branches continue into the same nodes — there is one pipeline, not one per department.",
        "height": 360,
        "width": 460
      },
      "name": "1 · Mailboxes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1060,
        -230
      ],
      "id": "e902f5ad-d404-4774-93a1-e4bef9093c51"
    },
    {
      "parameters": {
        "content": "## 2 · The only node you maintain\n**Load Master Data** holds two tables:\n\n**DEPARTMENTS** — seller, bank account, contact person, output paths, target format, error recipient, template.\n\n**CUSTOMERS** — e-mail address and Leitweg-ID / buyer reference per buyer.\n\n⚠️ The customer table is not optional. **Neither the buyer's e-mail address nor the Leitweg-ID is printed on an incoming PDF.** Without an entry there is no recipient and no buyer reference (BT-10, BR-DE-15) — the document goes to the error path instead of to the customer. That is intentional.",
        "height": 430,
        "width": 420
      },
      "name": "2 · Master data",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -580,
        -300
      ],
      "id": "0497bc98-47d6-4217-9bcc-71b7ab7603c2"
    },
    {
      "parameters": {
        "content": "## 3 · Reading without an LLM\n**Extract from File** pulls the text (`Join Pages` OFF — one PDF may carry several invoices, one per page). **Normalize Text** repairs hyphenation and spacing and flags scans without a text layer. **Extract Fields** applies a **rule set** — a JSON document that says where each value sits — and emits **one item per document**; pages without a document (attachments) are skipped.\n\nThe preset *Generic Invoice (DE)* reads typical Word/Excel invoices. A department with its own layout gets its own rule set in **Load Master Data** (`rules`), see `docs/RULES.md` of the node — data, not code.\n\nThis works because the documents come out of templates and look the same every time. Compared to a language model: **repeatable, free, no external service**.\n\nA rule that cannot find a field **does not guess** — it yields null and the field lands in `missing[]`. Missing fields surface in the next node, by name.\n\n⚠️ Do **not** set `Max Pages` on the Extract node. In n8n 2.x, `0` means *zero pages*, not *all pages* — the node returns empty text with a green status. Leave the option out.",
        "height": 600,
        "width": 560
      },
      "name": "3 · Reading",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -140,
        -410
      ],
      "id": "f738cb02-8e38-42d2-b1be-3760e4a89245"
    },
    {
      "parameters": {
        "content": "## 4 · Assemble and recompute\n**Assemble Invoice** joins three sources:\n\n| From | What |\n|---|---|\n| Document (`fields`) | number, dates, line items, totals, address |\n| Department (`seller`) | seller, VAT ID, bank account, contact |\n| Customer table | e-mail address, Leitweg-ID |\n\nThe numbers are **recomputed**: items → net → VAT → gross, tolerance 2 cents.\n\nCredit note detected → `type: credit_note` with **positive** amounts (EN 16931: the sign belongs to the document type, BR-27 forbids negative unit prices).\n\nA different VAT ID on the document than in master data means the mail hit the wrong mailbox — that is reported too.\n\nEvery finding is a `checks[]` entry (`PDF2INV_…`, severity error/warning); `ok` is false as soon as one error exists. **Approved?** reads `ok` together with the API's `valid` and `results[]`.",
        "height": 820,
        "width": 700
      },
      "name": "4 · Build",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        440,
        -680
      ],
      "id": "f703e9c7-a392-4d74-99c9-298dd43b2e6e"
    },
    {
      "parameters": {
        "content": "## 5 · Visual layout\n**Select PDF Template** builds the BlockTemplate per document, so long-form dates and country names can be rendered as finished text (the XML keeps the normalised values).\n\nThe template declares **`lengthUnit: 'pt'`** — always set it. A template without it is read in legacy mode: page margins and spacer heights become millimetres while everything else stays points.\n\n`locale` is set on **every summary row** — that is where number formatting is read (`18.391,71` instead of `18,391.71`).\n\nSet `templateId` in master data to use a template from your invoice-api.xhub account instead; then the layout is maintained in the account, not in the workflow.",
        "height": 480,
        "width": 460
      },
      "name": "5 · Template",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2480,
        -260
      ],
      "id": "e4ee42b5-ca4f-413e-b002-f17f8fdfa93d"
    },
    {
      "parameters": {
        "content": "## 6 · Output\nFingerprint → duplicate guard → (release) → Generate → store → send to the buyer → log.\n\nThe directories must **exist**, the write node does not create them:\n```\nmkdir -p /home/node/e-invoice/{in,out,log}/musterfirma\n```\nSelf-hosted n8n ≥ 1.x also needs `N8N_RESTRICT_FILE_ACCESS_TO=/home/node/e-invoice`.\n\n**Fingerprint** computes `payloadHash` — SHA-256 over the canonical invoice JSON, byte-identical to the `payloadHash` the API returns — and the file name `date_number_format_hash8.pdf`. Same document twice → same name. Not the API's `hash`: that is the SHA-256 of the generated bytes and changes on every call (PDF timestamp).\n\n**Duplicate guard across runs** — **Check Duplicate** looks the hash up in the department's JSON-Lines log **before** anything is generated. A `status: \"ok\"` line with the same hash means the document already went out: the run ends with a `status: \"duplicate\"` line and a short mail to the department, **no API call, nothing sent twice**.",
        "height": 620,
        "width": 620
      },
      "name": "6 · Output",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3320,
        -440
      ],
      "id": "484db0c1-0839-4ec8-9c9a-6dbe94e10ba3"
    },
    {
      "parameters": {
        "content": "## 7 · What does not pass\nThree gates, and the difference matters:\n\n1. **Assemble Invoice** — own checks (`PDF2INV_…`): totals add up, mandatory fields, customer known, VAT ID matches\n2. **Validate** — `valid` = object-level rules · `results[]` = XSD + Schematron per format. **Approved?** checks `valid` and the `results[]` of the department's **target format** family (a ZUGFeRD document is not held back by an XRechnung-only rule; every result still lands in the error mail).\n3. **Generate** — has an error output into the same path (HTTP 422 with field-level messages)\n\nOptional fourth gate: **Request Release (E-Mail)** — disabled by default, see the note next to it. A rejected release ends in the same error path.\n\nIf a document fails, **nothing is sent to the buyer**. Instead: a `status: \"error\"` line in the department's log, a plain-language mail **back to the department** that sent it, and the source PDF stays unchanged in the archive. Fix it, send the mail again.\n\nField names in 422 errors (`errors[].field`) arrive with community node ≥ 1.1.3.",
        "height": 470,
        "width": 640
      },
      "name": "7 · Errors",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1120,
        820
      ],
      "id": "ea7c4ec7-a049-47d6-b568-14292211eccd"
    },
    {
      "parameters": {
        "operation": "sendAndWait",
        "fromEmail": "={{ $('Assemble Invoice').item.json.department.senderMail }}",
        "toEmail": "={{ $('Assemble Invoice').item.json.department.releaseMail || $('Assemble Invoice').item.json.department.errorMail }}",
        "subject": "=[E-Invoice] Release requested: {{ $('Assemble Invoice').item.json.invoice.invoiceNumber }} — {{ $('Assemble Invoice').item.json.invoice.buyer.name }}",
        "message": "=A document passed all checks and is waiting for your release before it is generated and sent.\n\nDepartment: {{ $('Assemble Invoice').item.json.departmentKey }}\nDocument:   {{ $('Assemble Invoice').item.json.invoice.type === 'credit_note' ? 'Credit note' : 'Invoice' }} {{ $('Assemble Invoice').item.json.invoice.invoiceNumber }} dated {{ $('Assemble Invoice').item.json.invoice.issueDate }}\nBuyer:      {{ $('Assemble Invoice').item.json.invoice.buyer.name }}\nGross:      {{ $('Assemble Invoice').item.json.invoice.total }} {{ $('Assemble Invoice').item.json.invoice.currency }}\nSource:     {{ $('Assemble Invoice').item.json.sourceFile }}\n\nRelease = generate the e-invoice and send it to the buyer. Reject = nothing is sent; the department gets a note.",
        "responseType": "approval",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Release",
            "disapproveLabel": "Reject",
            "buttonApprovalStyle": "primary",
            "buttonDisapprovalStyle": "secondary"
          }
        },
        "options": {
          "appendAttribution": false,
          "limitWaitTime": {
            "values": {
              "limitType": "afterTimeInterval",
              "resumeAmount": 3,
              "resumeUnit": "days"
            }
          }
        }
      },
      "name": "Request Release (E-Mail)",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        2080,
        240
      ],
      "disabled": true,
      "webhookId": "release-06",
      "notes": "Optional four-eyes step. Enable this node to pause every document until a reviewer clicks Release in the mail.",
      "id": "5642eeb4-2ab0-4684-9791-6438e82f1a32"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "released",
              "leftValue": "={{ $json.data?.approved !== false }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Released?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2300,
        240
      ],
      "id": "c4880489-a752-4e94-b40a-04fc3e214093"
    },
    {
      "parameters": {
        "fileSelector": "={{ $('Assemble Invoice').item.json.department.logPath }}",
        "options": {
          "dataPropertyName": "log"
        }
      },
      "name": "Read Department Log",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1420,
        240
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "id": "39ed9dc5-1d12-4137-9083-1ad860adb12a"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "not-dup",
              "leftValue": "={{ $json.duplicate === true }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "New Document?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1860,
        240
      ],
      "id": "af8f6483-04f8-48ac-9ca6-37f2ebafa6ad"
    },
    {
      "parameters": {
        "jsCode": "// ── Prepare Duplicate Notice ──────────────────────────────────────────\n// Mode: \"Run Once for Each Item\"\n// The same document was already sent in an earlier run (same payloadHash in\n// the department's log). The guard sits BEFORE generation, so no API call is\n// spent on it; nothing goes to the buyer a second time; the department gets a\n// short note and the log records the duplicate.\n\nconst dup = $input.item.json;                   // Check Duplicate output\nconst doc = $('Assemble Invoice').item.json;\nconst fp = $('Fingerprint').item.json;\nconst dep = doc.department || {};\nconst d = doc.invoice || {};\nconst prev = dup.previous || {};\n\nconst entry = {\n  timestamp: new Date().toISOString(),\n  execution: $execution.id,\n  workflow: $workflow.name,\n  status: 'duplicate',\n\n  department: doc.departmentKey,\n  docId: doc.docId,\n  fingerprint: fp.payloadHash,\n  payloadHash: fp.payloadHash,\n  sourceFile: doc.sourceFile,\n  sourceMailFrom: doc.mail?.from ?? null,\n  sourceMailSubject: doc.mail?.subject ?? null,\n\n  invoiceNumber: d.invoiceNumber,\n  documentType: d.type,\n  grossAmount: d.total,\n  currency: d.currency,\n  buyer: d.buyer?.name,\n\n  duplicateOf: { execution: prev.execution ?? null, timestamp: prev.timestamp ?? null, sentTo: prev.sentTo ?? null },\n};\n\nconst mailText = [\n  'a document from your mailbox was already sent as an e-invoice in an earlier run.',\n  '',\n  `Department:   ${doc.departmentKey}`,\n  `Source file:  ${doc.sourceFile}`,\n  `Document:     ${d.invoiceNumber} (${d.type}) · ${d.total} ${d.currency}`,\n  `First sent:   ${prev.timestamp ?? '–'} (execution ${prev.execution ?? '–'})` + (prev.sentTo ? ` to ${prev.sentTo}` : ''),\n  `Execution:    ${$execution.id}`,\n  '',\n  'Nothing was generated or sent again. If the document really changed, the content',\n  'changes too and it gets a new fingerprint — then it goes out as a new document.',\n].join('\\n');\n\nreturn {\n  json: {\n    ...entry,\n    logPath: dep.logPath,\n    recipient: dep.errorMail,\n    sender: dep.senderMail,\n    subject: `[E-Invoice] Already sent: ${d.invoiceNumber}`,\n    mailText,\n  },\n  binary: {\n    logLine: {\n      data: Buffer.from(JSON.stringify(entry) + '\\n', 'utf8').toString('base64'),\n      mimeType: 'application/x-ndjson',\n      fileName: 'e-invoice.jsonl',\n    },\n  },\n};\n"
      },
      "name": "Prepare Duplicate Notice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2080,
        460
      ],
      "id": "3cf8e295-602d-4af7-a9f5-1dd307f8a09f"
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.logPath }}",
        "dataPropertyName": "logLine",
        "options": {
          "append": true
        }
      },
      "name": "Append Log (Duplicate)",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        2300,
        460
      ],
      "id": "d436e94c-1f6f-464a-aad3-a2e0b559f800"
    },
    {
      "parameters": {
        "fromEmail": "={{ $json.sender }}",
        "toEmail": "={{ $json.recipient }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.mailText }}",
        "options": {
          "appendAttribution": false
        }
      },
      "name": "Notify Department (Duplicate)",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        2520,
        460
      ],
      "credentials": {},
      "id": "96e82873-aecd-41b2-a276-639a01b50c71"
    },
    {
      "parameters": {
        "content": "## Release (optional)\n**Request Release (E-Mail)** pauses the run and mails the reviewer (`releaseMail` in master data, else `errorMail`) two buttons: **Release** generates and sends, **Reject** ends in the error path with a note to the department. Waits up to 3 days.\n\nOff by default — enable the node for a four-eyes principle. One mail per mailbox run: if a run carries several documents, they are released together.",
        "height": 300,
        "width": 420,
        "color": 4
      },
      "name": "5b · Release",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2040,
        -100
      ],
      "id": "1f32c790-c7db-4065-b60b-8d4407ed9d44"
    },
    {
      "parameters": {
        "operation": "normalizeText",
        "text": "={{ $json.text }}",
        "options": {
          "pageCount": "={{ $json.numpages ?? 0 }}",
          "pdfInfo": "={{ $json.info }}",
          "pdfMetadata": "={{ $json.metadata }}",
          "includeInput": true
        }
      },
      "id": "p2i-normalizeText",
      "name": "Normalize Text",
      "type": "n8n-nodes-pdf2invoice-api-xhub.pdf2InvoiceXhub",
      "typeVersion": 1,
      "position": [
        100,
        400
      ]
    },
    {
      "parameters": {
        "operation": "extractFields",
        "pages": "={{ $json.text }}",
        "rulesSource": "generic-invoice-de",
        "options": {
          "isLikelyScanned": "={{ $json.stats.isLikelyScanned }}",
          "includeInput": true,
          "noDocumentBehaviour": "item"
        }
      },
      "id": "p2i-extractFields",
      "name": "Extract Fields",
      "type": "n8n-nodes-pdf2invoice-api-xhub.pdf2InvoiceXhub",
      "typeVersion": 1,
      "position": [
        320,
        400
      ]
    },
    {
      "parameters": {
        "operation": "assembleInvoice",
        "fields": "={{ $json.fields }}",
        "documentType": "auto",
        "seller": "={{ JSON.stringify($json.department.seller) }}",
        "buyerSource": "customers",
        "customers": "={{ JSON.stringify($json.customers) }}",
        "defaults": {
          "dueDays": 0,
          "taxRate": 19,
          "unit": "C62",
          "requireBuyerReference": true,
          "tolerance": 0.02
        },
        "countryCode": "={{ $json.department.seller.countryCode }}",
        "options": {
          "includeInput": true,
          "failOnError": false
        }
      },
      "id": "p2i-assembleInvoice",
      "name": "Assemble Invoice",
      "type": "n8n-nodes-pdf2invoice-api-xhub.pdf2InvoiceXhub",
      "typeVersion": 1,
      "position": [
        540,
        400
      ]
    },
    {
      "parameters": {
        "operation": "fingerprint",
        "invoice": "={{ $('Assemble Invoice').item.json.invoice }}",
        "format": "={{ $('Assemble Invoice').item.json.department.targetFormat }}",
        "options": {
          "includeInput": true,
          "extension": "auto"
        }
      },
      "id": "p2i-fingerprint",
      "name": "Fingerprint",
      "type": "n8n-nodes-pdf2invoice-api-xhub.pdf2InvoiceXhub",
      "typeVersion": 1,
      "position": [
        1200,
        240
      ]
    },
    {
      "parameters": {
        "operation": "checkDuplicate",
        "payloadHash": "={{ $('Fingerprint').item.json.payloadHash }}",
        "logSource": "binary",
        "binaryPropertyName": "log",
        "options": {
          "includeInput": true,
          "hashField": "payloadHash",
          "fallbackHashField": "fingerprint",
          "okValues": "ok,generated,sent"
        }
      },
      "id": "p2i-checkDuplicate",
      "name": "Check Duplicate",
      "type": "n8n-nodes-pdf2invoice-api-xhub.pdf2InvoiceXhub",
      "typeVersion": 1,
      "position": [
        1640,
        240
      ]
    }
  ],
  "connections": {
    "Mailbox: Musterfirma (IMAP)": {
      "main": [
        [
          {
            "node": "Department: musterfirma",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Department: musterfirma": {
      "main": [
        [
          {
            "node": "Load Master Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Master Data": {
      "main": [
        [
          {
            "node": "Split PDF Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split PDF Attachments": {
      "main": [
        [
          {
            "node": "Archive Source PDF",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract from File (PDF)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File (PDF)": {
      "main": [
        [
          {
            "node": "Normalize Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Text": {
      "main": [
        [
          {
            "node": "Extract Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Fields": {
      "main": [
        [
          {
            "node": "Assemble Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble Invoice": {
      "main": [
        [
          {
            "node": "invoice-api.xhub: Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "invoice-api.xhub: Validate": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Fingerprint",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fingerprint": {
      "main": [
        [
          {
            "node": "Read Department Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Department Log": {
      "main": [
        [
          {
            "node": "Check Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Duplicate": {
      "main": [
        [
          {
            "node": "New Document?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New Document?": {
      "main": [
        [
          {
            "node": "Request Release (E-Mail)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Duplicate Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Release (E-Mail)": {
      "main": [
        [
          {
            "node": "Released?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Released?": {
      "main": [
        [
          {
            "node": "Select PDF Template",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select PDF Template": {
      "main": [
        [
          {
            "node": "invoice-api.xhub: Generate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "invoice-api.xhub: Generate": {
      "main": [
        [
          {
            "node": "Generated?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generated?": {
      "main": [
        [
          {
            "node": "Prepare Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Result": {
      "main": [
        [
          {
            "node": "Store E-Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store E-Invoice": {
      "main": [
        [
          {
            "node": "Send to Buyer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Buyer": {
      "main": [
        [
          {
            "node": "Build Log Line",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Log Line": {
      "main": [
        [
          {
            "node": "Append Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Duplicate Notice": {
      "main": [
        [
          {
            "node": "Append Log (Duplicate)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Log (Duplicate)": {
      "main": [
        [
          {
            "node": "Notify Department (Duplicate)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Error": {
      "main": [
        [
          {
            "node": "Append Error Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Error Log": {
      "main": [
        [
          {
            "node": "Notify Department",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
