{
  "name": "REVOPS-LI-005 | 03 | Protected merge approvals",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "GET",
        "path": "revops-li-005-pending",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "51d871d3-ec3d-43a3-b113-eb22d8761b62",
      "name": "List Pending Approvals",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "b13feff0-35a3-4f6f-af8d-74b0b4c969cd",
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_APPROVAL_SECRET_CREDENTIAL",
          "name": "Duplicate Approval Secret"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.pending_candidates() AS candidates;",
        "options": {
          "queryReplacement": "={{ [] }}"
        }
      },
      "id": "985cdced-43c8-4064-8ffa-54289dded0c3",
      "name": "Read Pending Candidates",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        270,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ {candidates:$json.candidates} }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "4646f182-2154-4dcf-a18b-e4c10eae8ee7",
      "name": "Return Pending List",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        540,
        0
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "revops-li-005-decision",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "4ed6dc3a-7eff-485b-87d3-319d0189e761",
      "name": "Approval Decision",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        810,
        0
      ],
      "webhookId": "c06c9158-a7cc-40d4-98f5-3dadaa778163",
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_APPROVAL_SECRET_CREDENTIAL",
          "name": "Duplicate Approval Secret"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const b=$input.first().json.body||{};const errors=[];if(!/^[0-9a-f-]{36}$/i.test(String(b.candidate_id||'')))errors.push('candidate_id UUID required');if(!/^[0-9a-f-]{36}$/i.test(String(b.approval_token||'')))errors.push('approval_token UUID required');if(!['APPROVE','REJECT'].includes(b.decision))errors.push('decision must be APPROVE or REJECT');if(b.decision==='APPROVE'&&!/^\\d+$/.test(String(b.primary_id||'')))errors.push('numeric primary_id required');if(!/^\\S+@\\S+\\.\\S+$/.test(String(b.approver_email||'')))errors.push('approver_email required');return [{json:{valid:!errors.length,errors,b}}];"
      },
      "id": "38c47cbc-be47-4434-89c9-a02b5182785b",
      "name": "Validate Decision",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f34d4ee1-65ac-4341-aae2-e82015988e0c",
              "leftValue": "={{ $json.valid === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cf1b8bdc-e752-4b73-b267-49c06026f66b",
      "name": "Decision Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1350,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.decide_candidate($1::uuid,$2::uuid,$3::text,$4::text,$5::text,$6::boolean,$7::text) AS result;",
        "options": {
          "queryReplacement": "={{ [$json.b.candidate_id,$json.b.approval_token,$json.b.decision,$json.b.primary_id||null,$json.b.approver_email,Boolean($json.b.manager_override),String($json.b.note||'')] }}"
        }
      },
      "id": "f0e9ce22-5361-4759-af37-5c25d51101f9",
      "name": "Apply One-Time Decision",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1620,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json.result }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "c508f6a8-4d4a-4310-87eb-47af27233025",
      "name": "Decision Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1890,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ {accepted:false,errors:$json.errors} }}",
        "options": {
          "responseCode": 400
        }
      },
      "id": "361ff666-dd30-4cdd-a5b9-8ab6bd0c32e2",
      "name": "Decision Rejected",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "content": "## Human approval gate\nBoth endpoints require the same n8n Header Auth credential. GET returns pending candidates and one-time tokens.\nPOST accepts APPROVE or REJECT. Approval must select one of the two record IDs as primary.\nTokens expire, are single-use and every decision is audited. Approval does not bypass merge blockers.",
        "width": 700,
        "height": 220
      },
      "id": "9cdfc3d3-4873-4c1b-86e2-dda28218bdc9",
      "name": "Workflow Guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -270
      ]
    }
  ],
  "connections": {
    "List Pending Approvals": {
      "main": [
        [
          {
            "node": "Read Pending Candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Pending Candidates": {
      "main": [
        [
          {
            "node": "Return Pending List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approval Decision": {
      "main": [
        [
          {
            "node": "Validate Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Decision": {
      "main": [
        [
          {
            "node": "Decision Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decision Valid?": {
      "main": [
        [
          {
            "node": "Apply One-Time Decision",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Decision Rejected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply One-Time Decision": {
      "main": [
        [
          {
            "node": "Decision Accepted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Tbilisi",
    "saveExecutionProgress": true
  },
  "active": false,
  "pinData": {},
  "tags": []
}
