{
  "id": "oaSeoDemo00000003",
  "name": "Ops Automators - Invoice review gate",
  "nodes": [
    {
      "id": "manual",
      "name": "Run sample",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        260
      ]
    },
    {
      "id": "sample",
      "name": "Sample records",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [\n  {\n    \"dealId\": \"D-104\",\n    \"billingVersion\": 1,\n    \"customerReference\": \"customer-demo-01\",\n    \"currency\": \"USD\",\n    \"approved\": true,\n    \"approvedTotalMinor\": 250000,\n    \"lineItems\": [\n      {\n        \"description\": \"Synthetic implementation milestone\",\n        \"quantity\": 1,\n        \"unitAmountMinor\": 250000\n      }\n    ]\n  },\n  {\n    \"dealId\": \"D-105\",\n    \"billingVersion\": 1,\n    \"customerReference\": \"customer-demo-02\",\n    \"currency\": \"USD\",\n    \"approved\": true,\n    \"approvedTotalMinor\": 175000,\n    \"lineItems\": [\n      {\n        \"description\": \"Synthetic mismatch example\",\n        \"quantity\": 1,\n        \"unitAmountMinor\": 170000\n      }\n    ]\n  }\n].map(json => ({ json }));"
      },
      "position": [
        240,
        260
      ]
    },
    {
      "id": "logic",
      "name": "Validate for review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const records = $input.all();\nconst keys = new Map();\nfor (const { json: row } of records) {\n  const key = JSON.stringify([row.dealId, row.billingVersion]);\n  keys.set(key, (keys.get(key) || 0) + 1);\n}\nreturn records.map(({ json: row }) => {\n  const reasons = [];\n  if (typeof row.dealId !== 'string' || !row.dealId.trim()) reasons.push('Missing deal ID');\n  if (!Number.isSafeInteger(row.billingVersion) || row.billingVersion < 1) reasons.push('Missing positive billing version');\n  if (typeof row.customerReference !== 'string' || !row.customerReference.trim()) reasons.push('Missing billing customer reference');\n  if (row.approved !== true) reasons.push('Commercial approval is required');\n  if (row.currency !== 'USD') reasons.push('This example supports USD only');\n  if (!Number.isSafeInteger(row.approvedTotalMinor) || row.approvedTotalMinor <= 0) reasons.push('Approved total must be positive integer cents');\n  const lines = Array.isArray(row.lineItems) ? row.lineItems : [];\n  if (!lines.length) reasons.push('No invoice lines');\n  let calculatedTotalMinor = 0;\n  for (const line of lines) {\n    if (!line || typeof line.description !== 'string' || !line.description.trim() || !Number.isSafeInteger(line.quantity) || line.quantity <= 0 || !Number.isSafeInteger(line.unitAmountMinor) || line.unitAmountMinor < 0) { reasons.push('Invalid line description, quantity, or unit amount'); continue; }\n    calculatedTotalMinor += line.quantity * line.unitAmountMinor;\n  }\n  if (!Number.isSafeInteger(calculatedTotalMinor)) reasons.push('Calculated total exceeds safe integer range');\n  if (calculatedTotalMinor !== row.approvedTotalMinor) reasons.push('Line total does not match approved total');\n  const key = JSON.stringify([row.dealId, row.billingVersion]);\n  if (keys.get(key) > 1) reasons.push('Duplicate deal/version in this batch');\n  const result = { dealId: row.dealId || null, status: reasons.length ? 'needs_review' : 'ready_for_review', reasons, calculatedTotalMinor };\n  if (!reasons.length) result.draft = { sourceKey: key, customerReference: row.customerReference, currency: row.currency, totalMinor: calculatedTotalMinor, lineItems: lines };\n  return { json: result };\n});"
      },
      "position": [
        500,
        260
      ]
    },
    {
      "id": "instructions",
      "name": "Read before connecting systems",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Ops Automators - Invoice review gate\n\nRun synthetic USD deals through validation. Only ready_for_review records contain a draft payload. needs_review records must go to a human exception queue. This template does not connect to Stripe, create invoices, charge customers, or guarantee idempotency across executions.\n\nSetup: https://www.opsautomators.com/blog/n8n-workflow-templates\n\nSynthetic demo data. No credentials required. MIT licensed.",
        "height": 260,
        "width": 740
      },
      "position": [
        0,
        -70
      ]
    }
  ],
  "connections": {
    "Run sample": {
      "main": [
        [
          {
            "node": "Sample records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample records": {
      "main": [
        [
          {
            "node": "Validate for review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
