{
  "name": "einvoice-allowance \u2014 refund webhook \u2192 svc/v1/allowance (retry + DLQ + audit)",
  "nodes": [
    {
      "parameters": {
        "content": "## einvoice-allowance (core) \u2014 v0.28.0 hardened\n\nSame fixes as `einvoice-issue-from-order`:\n- HTTP `fullResponse` so statusCode actually exists.\n- Dead-letter Slack now wired from `Exhausted? = true`.\n- State rehydration explicit.\n- Webhook responseMode = `responseNode` (fixed schema).\n- Asia/Taipei timezone set.\n\n**Body**\n```json\n{ \"provider\": \"amego\", \"input\": { \"invoiceNumber\": \"AB12345678\", \"items\": [...], \"amount\": {...}, \"taxType\": \"TAXABLE\" } }\n```\n\n**Why allowance does NOT need approval gate** (unlike void): allowance is reversible via `voidAllowance`. If your policy demands approval, copy `einvoice-void-with-approval` and swap endpoint.\n\n**Before activating**: replace `__REPLACE_ME__EINVOICE_SVC_URL__`, `__REPLACE_ME__GOOGLE_SHEET_ID__`, `__REPLACE_ME__SLACK_CHANNEL__`; attach httpHeaderAuth Credential.\n\n---\n\n## \u4e2d\u6587 \u2014 einvoice-allowance\uff08\u6838\u5fc3 / \u6298\u8b93\uff09\n\n**\u89f8\u767c**\uff1a\u9000\u6b3e\u7cfb\u7d71\u6253 `POST /webhook/einvoice/allowance`\u3002\n\n**Body**\uff1a\u898b\u4e0a\u65b9 JSON\u3002\n\n**\u6d41\u7a0b**\uff1a\u8ddf `einvoice-issue-from-order` \u540c\u578b \u2014 retry-with-backoff\u3001shape \u4e00\u81f4\uff0c\u53ea\u662f\u6253 `/v1/allowance`\u3002audit \u6a19 op=`allowance`\u3002\n\n**\u70ba\u4f55\u4e0d\u9700\u8981\u4eba\u5de5\u6838\u53ef\uff08\u8ddf void \u4e0d\u540c\uff09**\uff1a\u6298\u8b93\u672c\u8eab\u53ef\u88ab `voidAllowance` \u53cd\u8f49\u3002\u82e5\u4f60\u8ca1\u52d9\u653f\u7b56\u4ecd\u8981\u6c42\u6838\u53ef\uff0c\u8907\u88fd `einvoice-void-with-approval` \u6539 endpoint \u5373\u53ef\u3002\n\n**\u4e0a\u7dda\u524d\u9700\u66ff\u63db**\uff1a`__REPLACE_ME__EINVOICE_SVC_URL__`\u3001`__REPLACE_ME__GOOGLE_SHEET_ID__`\u3001`__REPLACE_ME__SLACK_CHANNEL__`",
        "height": 900,
        "width": 560
      },
      "id": "note-1",
      "name": "Pattern note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -740,
        -360
      ]
    },
    {
      "parameters": {
        "path": "einvoice/allowance",
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "wh-1",
      "name": "Webhook: allowance request",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        -120,
        -40
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const body = $json.body || $json;\nif (!body.provider || !body.input) throw new Error('provider + input required');\nreturn [{ json: {\n  correlationId: `inv-allow-${Date.now()}-${Math.floor(Math.random()*1e6)}`,\n  startedAt: new Date().toISOString(),\n  provider: body.provider,\n  input: body.input,\n  attempt: 0,\n  maxAttempts: 3,\n  backoffMs: [2000, 4000, 8000]\n}}];"
      },
      "id": "stamp-1",
      "name": "Stamp correlation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        120,
        -40
      ]
    },
    {
      "parameters": {
        "url": "=__REPLACE_ME__EINVOICE_SVC_URL__/v1/allowance",
        "method": "POST",
        "sendBody": true,
        "contentType": "json",
        "jsonBody": "={{ { provider: $json.provider, input: $json.input } }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "specifyBody": "json"
      },
      "id": "http-1",
      "name": "Call svc /v1/allowance",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        360,
        -40
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const state = $node['Stamp correlation'].json;\nconst statusCode = Number($json.statusCode ?? $json.status ?? 0);\nconst respBody = $json.body ?? $json;\nconst ok = statusCode >= 200 && statusCode < 300;\nreturn [{ json: { ...state, statusCode, ok, response: respBody } }];"
      },
      "id": "merge-1",
      "name": "Rehydrate state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        -40
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.ok }}",
              "value2": true
            }
          ]
        }
      },
      "id": "if-ok-1",
      "name": "Allowance ok?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        840,
        -40
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const s = $json;\nconst nextAttempt = (s.attempt ?? 0) + 1;\nif (nextAttempt > s.maxAttempts) return [{ json: { ...s, attempt: nextAttempt, exhausted: true } }];\nreturn [{ json: { ...s, attempt: nextAttempt, waitMs: s.backoffMs[nextAttempt - 1] } }];"
      },
      "id": "incr-1",
      "name": "Increment + decide",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        120
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.exhausted === true }}"
            }
          ]
        }
      },
      "id": "if-ex-1",
      "name": "Exhausted?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1320,
        120
      ]
    },
    {
      "parameters": {
        "amount": "={{ $json.waitMs }}",
        "unit": "ms"
      },
      "id": "wait-1",
      "name": "Wait backoff",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1560,
        240
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": "__REPLACE_ME__GOOGLE_SHEET_ID__",
        "sheetName": "Audit",
        "columnsUi": {
          "columnValues": [
            {
              "column": "correlationId",
              "value": "={{ $json.correlationId }}"
            },
            {
              "column": "op",
              "value": "allowance"
            },
            {
              "column": "provider",
              "value": "={{ $json.provider }}"
            },
            {
              "column": "invoiceNumber",
              "value": "={{ $json.input?.invoiceNumber }}"
            },
            {
              "column": "status",
              "value": "={{ $json.exhausted ? 'failed-dlq' : 'allowed' }}"
            },
            {
              "column": "result",
              "value": "={{ JSON.stringify($json.exhausted ? null : $json.response?.result) }}"
            },
            {
              "column": "appliedAt",
              "value": "={{ new Date().toISOString() }}"
            }
          ]
        }
      },
      "id": "audit-1",
      "name": "Audit row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1320,
        -120
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "channel": "__REPLACE_ME__SLACK_CHANNEL__",
        "text": "=\u26a0\ufe0f einvoice allowance dead-letter \u2014 correlation: `{{ $json.correlationId }}` invoice=`{{ $json.input?.invoiceNumber }}` provider=`{{ $json.provider }}`"
      },
      "id": "slack-dlq-1",
      "name": "Slack dead-letter",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1560,
        60
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: !$json.exhausted, correlationId: $json.correlationId, status: $json.exhausted ? 'failed-dlq' : 'allowed', allowanceNumber: $json.response?.result?.allowanceNumber ?? null }) }}",
        "options": {}
      },
      "id": "respond-1",
      "name": "Respond to caller",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1560,
        -120
      ]
    }
  ],
  "connections": {
    "Webhook: allowance request": {
      "main": [
        [
          {
            "node": "Stamp correlation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp correlation": {
      "main": [
        [
          {
            "node": "Call svc /v1/allowance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call svc /v1/allowance": {
      "main": [
        [
          {
            "node": "Rehydrate state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rehydrate state": {
      "main": [
        [
          {
            "node": "Allowance ok?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Allowance ok?": {
      "main": [
        [
          {
            "node": "Audit row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Increment + decide",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Increment + decide": {
      "main": [
        [
          {
            "node": "Exhausted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Exhausted?": {
      "main": [
        [
          {
            "node": "Audit row",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack dead-letter",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait backoff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait backoff": {
      "main": [
        [
          {
            "node": "Call svc /v1/allowance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Audit row": {
      "main": [
        [
          {
            "node": "Respond to caller",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Taipei"
  },
  "active": false,
  "tags": []
}