{
  "name": "Log Invoice Line Items and Verify Totals with n8n (free JSON)",
  "nodes": [
    {
      "id": "c63c136a-ae4f-471c-b846-3d2e42f6d818",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1536,
        -224
      ],
      "parameters": {
        "color": 4,
        "width": 684,
        "height": 1268,
        "content": "## Invoice PDF line items and total check\n\nFor finance and operations teams who want invoice PDFs turned into structured rows, with a check on the totals. Upload an invoice PDF via a form. An OpenAI model reads the extracted text and returns structured JSON with all invoice details — vendor, invoice number, dates, PO number, GST details, and every line item (description, quantity, unit price, HSN, GST%). The workflow then checks that the sum of all line totals matches the invoice total, logs the header and every line to Google Sheets, and emails you a complete extraction report.\n\n## How it works\n- **1. Form — Upload Invoice PDF** collects your name, email, the PDF, currency, and optional notes\n- **2. Extract From File — PDF to Text** converts the PDF binary to text\n- **3. HTTP — AI Extract Invoice Fields** sends the text to an OpenAI model and returns structured JSON\n- **4. Code — Parse Invoice JSON Safely** parses and validates the JSON and attaches the form details\n- **5a. Sheets — Log Invoice Header** appends one header row (parallel branch)\n- **5b. Split Out — Line Items** separates each line item (parallel branch)\n- **6. Code — Add Unique Key and Line Metadata** adds a unique key and carries invoice context to each line\n- **7. Sheets — Log Each Line Item** appends one row per line item\n- **8. Aggregate — Collect All Line Items** bundles the lines back into one item\n- **9. Code — Verify Totals and Build Report** checks the line-sum against the invoice total and builds the HTML report\n- **10. Gmail — Send Extraction Report** emails the report\n\n## Sheet setup (two tabs in one sheet)\n- Tab **Invoices**: Date, Invoice No, Vendor, Invoice Date, PO Number, Total Amount, Taxable Amount, Total CGST, Total SGST, Line Items Count, Currency, Uploaded By\n- Tab **Line Items**: Date, Invoice No, Vendor, SR No, Description, HSN Code, Quantity, Unit Price, Line Total, CGST %, SGST %, Unique Key\n\n## Set up steps\n1. In **3. HTTP — AI Extract Invoice Fields** — connect your OpenAI API credential. An OpenAI model is used for the extraction\n2. In **5a. Sheets — Log Invoice Header** and **7. Sheets — Log Each Line Item** — connect Google Sheets OAuth2 and replace `YOUR_INVOICE_LOG_SHEET_ID` in both. Create the two tabs above\n3. In **10. Gmail — Send Extraction Report** — connect Gmail OAuth2. The recipient comes from the form\n4. Activate the workflow, open the Form URL, upload an invoice PDF, and submit. The report is logged and emailed automatically\n\nSource: based on a template from the n8n Library at n8n.io, documented by LeenLabs.\nNot tested on live APIs."
      },
      "typeVersion": 1
    },
    {
      "id": "908fb953-9f3e-4004-8a33-711942aae766",
      "name": "Section — Upload, Extract, and AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -768,
        -16
      ],
      "parameters": {
        "color": 5,
        "width": 708,
        "height": 692,
        "content": "## Upload, Extract, and AI\nForm accepts the PDF upload. Extract From File converts the PDF to text. An OpenAI model then extracts all invoice fields as structured JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "efb797a6-bd64-4aed-bd84-61e8aec9dfd3",
      "name": "Section — Parse, Split, and Log",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -176
      ],
      "parameters": {
        "color": 6,
        "width": 900,
        "height": 1028,
        "content": "## Parse, Split, and Log\nCode parses the JSON safely. One branch logs the invoice header. The other splits each line item, adds a unique key, and logs every line to the second tab."
      },
      "typeVersion": 1
    },
    {
      "id": "dbbb73dd-f965-44c5-89a9-ef120ac4ea7c",
      "name": "Section — Verify and Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        -48
      ],
      "parameters": {
        "color": 4,
        "width": 740,
        "height": 708,
        "content": "## Verify and Email\nAggregate collects all line items. Code verifies the line-sum equals the invoice total within tolerance. Gmail sends the full extraction report with a verified or mismatch status."
      },
      "typeVersion": 1
    },
    {
      "id": "6af86046-b05e-44ce-a658-74357489aef6",
      "name": "1. Form — Upload Invoice PDF",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -704,
        224
      ],
      "parameters": {
        "options": {
          "path": "invoice-processor",
          "buttonLabel": "Process Invoice",
          "appendAttribution": false
        },
        "formTitle": "Invoice PDF Processor",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Your Name",
              "placeholder": "e.g. Jane Smith",
              "requiredField": true
            },
            {
              "fieldLabel": "Your Email",
              "placeholder": "your@email.com — extraction report will be sent here",
              "requiredField": true
            },
            {
              "fieldType": "file",
              "fieldLabel": "Invoice PDF",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Currency",
              "fieldOptions": {
                "values": [
                  {
                    "option": "INR — Indian Rupee"
                  },
                  {
                    "option": "USD — US Dollar"
                  },
                  {
                    "option": "EUR — Euro"
                  },
                  {
                    "option": "GBP — British Pound"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Notes",
              "placeholder": "Optional — any context about this invoice batch or vendor"
            }
          ]
        },
        "formDescription": "Upload a vendor invoice PDF. AI will extract all line items, validate totals, log to Google Sheets, and email you a full extraction report."
      },
      "typeVersion": 2.2
    },
    {
      "id": "f12462ee-3de1-4f57-96d6-d0a81b8b373f",
      "name": "2. Extract From File — PDF to Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -464,
        224
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1.1
    },
    {
      "id": "c9096ea0-90cd-48b4-b278-8c070cbd7709",
      "name": "3. HTTP — AI Extract Invoice Fields",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -224,
        224
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={\n  \"model\": \"gpt-4o-mini\",\n  \"temperature\": 0.1,\n  \"max_tokens\": 4000,\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a document parsing assistant. Extract structured data from invoice text and return ONLY valid raw JSON. No markdown, no code fences, no explanation text. Output must start with { and end with }.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify(\"Extract all fields from this invoice. Return ONLY valid JSON with this exact structure:\\n{\\n  \\\"invoice_number\\\": \\\"string\\\",\\n  \\\"vendor_name\\\": \\\"string\\\",\\n  \\\"invoice_date\\\": \\\"DD/MM/YYYY\\\",\\n  \\\"po_number\\\": \\\"string or null\\\",\\n  \\\"po_date\\\": \\\"DD/MM/YYYY or null\\\",\\n  \\\"total_amount\\\": float,\\n  \\\"taxable_amount\\\": float or null,\\n  \\\"total_cgst\\\": float or null,\\n  \\\"total_sgst\\\": float or null,\\n  \\\"line_items\\\": [\\n    {\\n      \\\"serial_no\\\": \\\"string\\\",\\n      \\\"description\\\": \\\"string — item name without HSN code\\\",\\n      \\\"hsn_code\\\": \\\"string or null\\\",\\n      \\\"quantity\\\": float,\\n      \\\"unit_price\\\": float,\\n      \\\"line_total\\\": float,\\n      \\\"cgst_pct\\\": \\\"string e.g. 9% or null\\\",\\n      \\\"sgst_pct\\\": \\\"string e.g. 9% or null\\\"\\n    }\\n  ]\\n}\\n\\nRules:\\n- Extract EVERY line item — do not skip any\\n- line_total must equal quantity multiplied by unit_price\\n- total_amount is the final payable amount including all taxes\\n- If a field is not found, use null\\n- Return ONLY the JSON object\\n\\nInvoice text:\\n\" + ($('2. Extract From File — PDF to Text').first().json.text || '')) }}\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "a753efd7-27a3-40ee-b4ee-11a5cc45b774",
      "name": "4. Code — Parse Invoice JSON Safely",
      "type": "n8n-nodes-base.code",
      "position": [
        32,
        224
      ],
      "parameters": {
        "jsCode": "const resp     = $input.first().json;\nconst formData = $('1. Form — Upload Invoice PDF').first().json;\n\nconst raw = resp?.choices?.[0]?.message?.content || '';\n\nif (!raw) throw new Error('AI returned empty response. Check your OpenAI API key and credits.');\n\nlet invoice = {};\ntry {\n  const cleaned = raw.trim()\n    .replace(/^```json\\s*/i, '')\n    .replace(/^```\\s*/i, '')\n    .replace(/```$/i, '')\n    .trim();\n  const start = cleaned.indexOf('{');\n  const end   = cleaned.lastIndexOf('}');\n  if (start === -1 || end === -1) throw new Error('No JSON object found in response');\n  invoice = JSON.parse(cleaned.substring(start, end + 1));\n} catch (e) {\n  throw new Error('JSON parsing failed: ' + e.message + ' | Raw: ' + raw.substring(0, 300));\n}\n\nif (!invoice.invoice_number) throw new Error('Invoice number not found in extraction. PDF may be image-only — OCR required.');\nif (!Array.isArray(invoice.line_items) || invoice.line_items.length === 0) {\n  throw new Error('No line items extracted. Check PDF quality or invoice format.');\n}\n\nconst submitter   = (formData['Your Name']  || '').trim();\nconst email       = (formData['Your Email'] || '').trim();\nconst currency    = (formData['Currency']   || 'INR').split('—')[0].trim();\nconst notes       = (formData['Notes']      || '').trim();\nconst processedAt = new Date().toISOString();\nconst dateOnly    = processedAt.split('T')[0];\n\nreturn [{\n  json: {\n    ...invoice,\n    submitter,\n    email,\n    currency,\n    notes,\n    processedAt,\n    dateOnly\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bfda95c7-0753-4d32-a6ba-fc4750da3a95",
      "name": "5a. Sheets — Log Invoice Header",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        272,
        64
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.dateOnly }}",
            "Vendor": "={{ $json.vendor_name }}",
            "Currency": "={{ $json.currency }}",
            "PO Number": "={{ $json.po_number || '' }}",
            "Invoice No": "={{ $json.invoice_number }}",
            "Total CGST": "={{ $json.total_cgst || '' }}",
            "Total SGST": "={{ $json.total_sgst || '' }}",
            "Uploaded By": "={{ $json.submitter }}",
            "Invoice Date": "={{ $json.invoice_date }}",
            "Total Amount": "={{ $json.total_amount }}",
            "Taxable Amount": "={{ $json.taxable_amount || '' }}",
            "Line Items Count": "={{ $json.line_items.length }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice No",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice No",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vendor",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Vendor",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "PO Number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "PO Number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total Amount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total Amount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Taxable Amount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Taxable Amount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total CGST",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total CGST",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total SGST",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total SGST",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Line Items Count",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Line Items Count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Uploaded By",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Uploaded By",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Invoices"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_INVOICE_LOG_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4a2cc96f-966c-4330-a542-159c91119e68",
      "name": "5b. Split Out — Line Items",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        272,
        272
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "line_items"
      },
      "typeVersion": 1
    },
    {
      "id": "bd83cbed-1b29-4082-8399-790a0e0ae9ce",
      "name": "6. Code — Add Unique Key and Line Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        512,
        272
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item    = $input.item.json;\nconst invoice = $('4. Code — Parse Invoice JSON Safely').first().json;\nconst idx     = $itemIndex;\n\nconst uniqueKey = (invoice.invoice_number || 'INV') + '-' + idx;\n\nconst calcTotal = parseFloat(item.quantity || 0) * parseFloat(item.unit_price || 0);\nconst lineTotal = parseFloat(item.line_total || 0);\nconst lineTotalMatch = Math.abs(calcTotal - lineTotal) < 0.5;\n\nreturn {\n  json: {\n    uniqueKey,\n    invoiceNo:    invoice.invoice_number,\n    vendor:       invoice.vendor_name,\n    invoiceDate:  invoice.invoice_date,\n    currency:     invoice.currency,\n    dateOnly:     invoice.dateOnly,\n    submitter:    invoice.submitter,\n    email:        invoice.email,\n    serialNo:     item.serial_no || String(idx + 1),\n    description:  item.description || '',\n    hsnCode:      item.hsn_code    || '',\n    quantity:     parseFloat(item.quantity)   || 0,\n    unitPrice:    parseFloat(item.unit_price) || 0,\n    lineTotal:    lineTotal,\n    cgstPct:      item.cgst_pct || '',\n    sgstPct:      item.sgst_pct || '',\n    lineTotalMatch,\n    invoiceTotal: parseFloat(invoice.total_amount) || 0\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c762da7e-055e-4f93-aec5-8e4255b4eca0",
      "name": "7. Sheets — Log Each Line Item",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        752,
        272
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.dateOnly }}",
            "SR No": "={{ $json.serialNo }}",
            "CGST %": "={{ $json.cgstPct }}",
            "SGST %": "={{ $json.sgstPct }}",
            "Vendor": "={{ $json.vendor }}",
            "HSN Code": "={{ $json.hsnCode }}",
            "Quantity": "={{ $json.quantity }}",
            "Invoice No": "={{ $json.invoiceNo }}",
            "Line Total": "={{ $json.lineTotal }}",
            "Unique Key": "={{ $json.uniqueKey }}",
            "Unit Price": "={{ $json.unitPrice }}",
            "Description": "={{ $json.description }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice No",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice No",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vendor",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Vendor",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SR No",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SR No",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "HSN Code",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "HSN Code",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Quantity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Quantity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Unit Price",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Unit Price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Line Total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Line Total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CGST %",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "CGST %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SGST %",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SGST %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Unique Key",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Unique Key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Line Items"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_INVOICE_LOG_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "ef4c76a0-45d8-4b78-9880-f06d905a189b",
      "name": "8. Aggregate — Collect All Line Items",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        992,
        272
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "allItems"
      },
      "typeVersion": 1
    },
    {
      "id": "df9dfbd4-69c7-4320-993a-7468a5d0051b",
      "name": "9. Code — Verify Totals and Build Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1232,
        272
      ],
      "parameters": {
        "jsCode": "// The lines are read from step 6 (not from the output of the Sheets step), so the report does not depend on what the Sheets step returns.\nconst all      = $('6. Code — Add Unique Key and Line Metadata').all().map(i => i.json);\nconst first    = all[0] || {};\nconst currency = (first.currency || 'INR').split('—')[0].trim();\nconst symbol   = currency === 'USD' ? '$' : currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : 'Rs ';\n\n// Text from the PDF goes into an email, so special characters are escaped first.\nconst esc = (v) => String(v ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n\nconst sumOfLineTotals = all.reduce((s, i) => s + parseFloat(i.lineTotal || 0), 0);\nconst invoiceTotal    = parseFloat(first.invoiceTotal || 0);\nconst difference      = Math.abs(sumOfLineTotals - invoiceTotal);\nconst totalVerified   = difference < 1.0;\n\nconst fmtNum = (n) => Number(n || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n\nconst lineRows = all.map((item, i) =>\n  '<tr>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;color:#1a1a2e;\">' + esc(item.serialNo || (i+1)) + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;color:#333;\">' + esc(item.description || '') + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:center;color:#666;\">' + esc(item.hsnCode || '-') + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:right;color:#333;\">' + (item.quantity || 0) + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:right;color:#333;\">' + symbol + fmtNum(item.unitPrice) + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:right;font-weight:600;color:#1a1a2e;\">' + symbol + fmtNum(item.lineTotal) + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:center;color:#666;\">' + esc(item.cgstPct || '-') + '</td>' +\n  '<td style=\"padding:7px 10px;border-bottom:1px solid #eee;font-size:12px;text-align:center;color:#666;\">' + esc(item.sgstPct || '-') + '</td>' +\n  '</tr>'\n).join('');\n\nconst verifyBanner = totalVerified\n  ? '<div style=\"background:#e8f5e9;border-left:4px solid #43a047;padding:12px 18px;border-radius:0 6px 6px 0;margin-bottom:20px;\"><strong style=\"color:#1b5e20;\">Total Verified</strong> — Sum of line items (' + symbol + fmtNum(sumOfLineTotals) + ') matches invoice total (' + symbol + fmtNum(invoiceTotal) + ').</div>'\n  : '<div style=\"background:#fff8e1;border-left:4px solid #f57f17;padding:12px 18px;border-radius:0 6px 6px 0;margin-bottom:20px;\"><strong style=\"color:#e65100;\">Total Mismatch</strong> — Sum of line items (' + symbol + fmtNum(sumOfLineTotals) + ') differs from invoice total (' + symbol + fmtNum(invoiceTotal) + ') by ' + symbol + fmtNum(difference) + '. Some line items may be missing from the extraction.</div>';\n\nconst htmlEmail = '<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head>' +\n'<body style=\"font-family:Arial,sans-serif;max-width:900px;margin:0 auto;background:#f5f5f5;\">' +\n\n'<div style=\"background:#1a1a2e;padding:24px 28px;border-radius:8px 8px 0 0;\">' +\n'<h1 style=\"color:#fff;font-size:20px;margin:0;\">Invoice Extraction Report</h1>' +\n'<p style=\"color:#90caf9;font-size:13px;margin:6px 0 0;\">Processed by ' + esc(first.submitter) + ' on ' + esc(first.dateOnly) + '</p>' +\n'</div>' +\n\n'<div style=\"background:#263159;padding:10px 24px;display:flex;gap:24px;flex-wrap:wrap;\">' +\n'<span style=\"color:#fff;font-size:13px;\">' + esc(first.vendor) + '</span>' +\n'<span style=\"color:#fff;font-size:13px;\">' + esc(first.invoiceNo) + '</span>' +\n'<span style=\"color:#fff;font-size:13px;\">' + esc(first.invoiceDate) + '</span>' +\n'<span style=\"color:#90caf9;font-size:13px;font-weight:700;\">' + symbol + fmtNum(invoiceTotal) + '</span>' +\n'</div>' +\n\n'<div style=\"background:#fff;padding:20px 24px;\">' +\n\nverifyBanner +\n\n'<h2 style=\"font-size:15px;color:#1a1a2e;border-bottom:2px solid #eee;padding-bottom:8px;margin:0 0 14px;\">Line Items (' + all.length + ' extracted)</h2>' +\n'<div style=\"overflow-x:auto;\">' +\n'<table style=\"width:100%;border-collapse:collapse;font-size:13px;min-width:600px;\">' +\n'<thead><tr style=\"background:#f0f4ff;\">' +\n'<th style=\"padding:9px 10px;text-align:left;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">SR</th>' +\n'<th style=\"padding:9px 10px;text-align:left;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">Description</th>' +\n'<th style=\"padding:9px 10px;text-align:center;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">HSN</th>' +\n'<th style=\"padding:9px 10px;text-align:right;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">Qty</th>' +\n'<th style=\"padding:9px 10px;text-align:right;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">Unit Price</th>' +\n'<th style=\"padding:9px 10px;text-align:right;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">Line Total</th>' +\n'<th style=\"padding:9px 10px;text-align:center;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">CGST</th>' +\n'<th style=\"padding:9px 10px;text-align:center;color:#444;border-bottom:2px solid #ddd;font-size:11px;\">SGST</th>' +\n'</tr></thead><tbody>' + lineRows + '</tbody>' +\n'<tfoot><tr style=\"background:#f0f4ff;\">' +\n'<td colspan=\"5\" style=\"padding:10px;text-align:right;font-weight:700;font-size:13px;color:#1a1a2e;\">TOTAL</td>' +\n'<td style=\"padding:10px;text-align:right;font-weight:700;font-size:14px;color:#1a1a2e;\">' + symbol + fmtNum(sumOfLineTotals) + '</td>' +\n'<td colspan=\"2\"></td>' +\n'</tr></tfoot>' +\n'</table></div>' +\n\n'</div>' +\n\n'<div style=\"background:#1a1a2e;padding:12px 24px;text-align:center;border-radius:0 0 8px 8px;\">' +\n'<p style=\"color:#90caf9;font-size:11px;margin:0;\">Invoice Processor — n8n + OpenAI + Google Sheets</p>' +\n'</div>' +\n\n'</body></html>';\n\nreturn [{\n  json: {\n    email:           first.email,\n    invoiceNo:       first.invoiceNo,\n    vendor:          first.vendor,\n    lineCount:       all.length,\n    invoiceTotal,\n    sumOfLineTotals: +sumOfLineTotals.toFixed(2),\n    totalVerified,\n    difference:      +difference.toFixed(2),\n    htmlEmail,\n    emailSubject:    (totalVerified ? '[Verified] ' : '[Check Totals] ') + 'Invoice Extracted — ' + (first.invoiceNo || '') + ' | ' + (first.vendor || '') + ' | ' + symbol + fmtNum(invoiceTotal)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "48075d92-d433-4298-be80-3024d5711bb4",
      "name": "10. Gmail — Send Extraction Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1472,
        272
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "={{ $json.htmlEmail }}",
        "options": {
          "senderName": "Invoice Processor"
        },
        "subject": "={{ $json.emailSubject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "3c8e1a77-52d4-4b1f-9a6e-0d7b4c2e8f15",
      "name": "Before you turn it on",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1536,
        1080
      ],
      "parameters": {
        "color": 3,
        "width": 684,
        "height": 640,
        "content": "## Before you turn it on\n\n1. Create the two tabs with the exact column names listed in the Overview note, and replace YOUR_INVOICE_LOG_SHEET_ID in both Sheets steps.\n2. The report is emailed to the address typed in the form. The form link is public by default, so anyone who has it can make your Gmail account send an email to any address. Use the form's authentication option if your n8n version has one, or keep the workflow switched off when unused.\n3. The tax fields (GST, CGST, SGST, HSN) and the DD/MM/YYYY date format are built for Indian invoices. For other invoices these fields are usually empty.\n4. The total check compares the sum of the line totals with the invoice total. On invoices where tax is added on top of the lines, it can show a mismatch even when the invoice is right.\n5. The model name (gpt-4o-mini) may not be available on your OpenAI account. Check and change it in step 3.\n6. Any error stops the run. Nothing is logged after the failing step, and the person who submitted the form is not told.\n7. Invoices are sent to OpenAI and written to Google Sheets. Uploading the same invoice twice creates duplicate rows."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "5b. Split Out — Line Items": {
      "main": [
        [
          {
            "node": "6. Code — Add Unique Key and Line Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Form — Upload Invoice PDF": {
      "main": [
        [
          {
            "node": "2. Extract From File — PDF to Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Sheets — Log Each Line Item": {
      "main": [
        [
          {
            "node": "8. Aggregate — Collect All Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Extract From File — PDF to Text": {
      "main": [
        [
          {
            "node": "3. HTTP — AI Extract Invoice Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. HTTP — AI Extract Invoice Fields": {
      "main": [
        [
          {
            "node": "4. Code — Parse Invoice JSON Safely",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Code — Parse Invoice JSON Safely": {
      "main": [
        [
          {
            "node": "5a. Sheets — Log Invoice Header",
            "type": "main",
            "index": 0
          },
          {
            "node": "5b. Split Out — Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8. Aggregate — Collect All Line Items": {
      "main": [
        [
          {
            "node": "9. Code — Verify Totals and Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9. Code — Verify Totals and Build Report": {
      "main": [
        [
          {
            "node": "10. Gmail — Send Extraction Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Code — Add Unique Key and Line Metadata": {
      "main": [
        [
          {
            "node": "7. Sheets — Log Each Line Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  }
}