{
  "name": "Invoice Follow-up — Tally Manual Payment (C)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "tally-manual-payment",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-tally-1",
      "name": "Webhook — Tally Inbound",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Tally posts { data: { fields: [{ label, value, ... }] } }\nconst body = $input.first().json.body || $input.first().json;\nconst fields = body.data?.fields || [];\n\nfunction byLabel(label) {\n  const f = fields.find(x => (x.label || '').toLowerCase() === label.toLowerCase());\n  return f ? f.value : null;\n}\n\nconst invoiceNumber = byLabel('invoice_number');\nconst paymentDateRaw = byLabel('payment_date');\nconst paymentMethod = byLabel('payment_method');\n\nif (!invoiceNumber) {\n  throw new Error('Tally payload missing invoice_number field.');\n}\n\nconst paymentDate = paymentDateRaw || new Date().toISOString().slice(0, 10);\n\nreturn { invoiceNumber, paymentDate, paymentMethod };"
      },
      "id": "code-extract-tally-1",
      "name": "Extract Form Fields",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE invoices SET status = 'paid', paid_at = $1::timestamp WHERE invoice_number = $2 AND status != 'paid' RETURNING id, invoice_number, client_name, client_email, amount, currency, airtable_record_id",
        "options": {
          "queryReplacement": "={{ $json.paymentDate }},{{ $json.invoiceNumber }}"
        }
      },
      "id": "pg-mark-paid-2",
      "name": "Postgres — Mark Paid Manually",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        640,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "leftValue": "={{ $items().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "if-row-updated-2",
      "name": "IF — Row Updated?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        860,
        300
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.airtable.com/v0/{{ $env.AIRTABLE_BASE_ID }}/{{ $env.AIRTABLE_TABLE_NAME }}/{{ $json.airtable_record_id }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ fields: { status: 'paid' } }) }}",
        "options": {}
      },
      "id": "airtable-paid-2",
      "name": "Airtable — Mark Paid",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1080,
        200
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.client_email }}",
        "subject": "=Payment received — Invoice {{ $json.invoice_number }}",
        "emailType": "text",
        "message": "=Hi {{ $json.client_name }},\n\nWe've received your payment for invoice {{ $json.invoice_number }} ({{ $json.currency }} {{ $json.amount }}). Thank you!\n\nLet me know if you need anything else.\n\nBest,\n{{ $env.OWNER_NAME }}\n{{ $env.BUSINESS_NAME }}",
        "options": {}
      },
      "id": "gmail-confirm-2",
      "name": "Gmail — Send Confirmation",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1300,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.GOTIFY_URL }}/message?token={{ $env.GOTIFY_TOKEN }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ title: 'Tally manual payment — invoice not found', message: 'Invoice number ' + $('Extract Form Fields').first().json.invoiceNumber + ' did not match any unpaid invoice.', priority: 6 }) }}",
        "options": {}
      },
      "id": "gotify-notfound-1",
      "name": "Gotify — Invoice Not Found",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1080,
        400
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, updated: true }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-200-ok-2",
      "name": "Respond — 200 OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1520,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, updated: false, reason: 'invoice_not_found' }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-200-notfound-1",
      "name": "Respond — 200 Not Found",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1300,
        400
      ]
    }
  ],
  "connections": {
    "Webhook — Tally Inbound": {
      "main": [
        [
          {
            "node": "Extract Form Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Form Fields": {
      "main": [
        [
          {
            "node": "Postgres — Mark Paid Manually",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres — Mark Paid Manually": {
      "main": [
        [
          {
            "node": "IF — Row Updated?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF — Row Updated?": {
      "main": [
        [
          {
            "node": "Airtable — Mark Paid",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gotify — Invoice Not Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable — Mark Paid": {
      "main": [
        [
          {
            "node": "Gmail — Send Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail — Send Confirmation": {
      "main": [
        [
          {
            "node": "Respond — 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gotify — Invoice Not Found": {
      "main": [
        [
          {
            "node": "Respond — 200 Not Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
