{
  "name": "RFP 04 - Risk Approval and SME Dispatcher",
  "nodes": [
    {
      "id": "b6bc042304c82567",
      "name": "Approval Boundary",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1280,
        -470
      ],
      "parameters": {
        "content": "## Risk, Approval & SME Dispatcher\nOnly items routed to REVIEW are sent to the owner/SME. The message contains the requirement, draft, evidence IDs, confidence and deterministic reasons. Approval changes status; it never submits a proposal externally.",
        "width": 820,
        "height": 320,
        "color": 5
      }
    },
    {
      "id": "9d48f663ea293ad3",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -1200,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      }
    },
    {
      "id": "4de47169d28935c0",
      "name": "Read Pending Review Queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -940,
        0
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SET_OPERATIONS_WORKBOOK_ID"
        },
        "sheetName": {
          "mode": "name",
          "value": "Review Queue"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      },
      "alwaysOutputData": true
    },
    {
      "id": "22d54358b7cf0029",
      "name": "Select Unnotified Reviews",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -680,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return $input.all().map(i=>i.json).filter(x=>String(x.status).toUpperCase()===\"PENDING_REVIEW\"&&!x.owner_notified_at).slice(0,25).map(x=>({json:{...x,review_message:`🧭 <b>RFP EVIDENCE REVIEW</b>\\n\\nProject: ${x.project_id}\\nRequirement: ${x.requirement_id}\\nCategory: ${x.category}\\nConfidence: ${x.confidence}\\nReasons: ${x.control_reasons}\\nEvidence: ${x.evidence_ids||\"None\"}\\n\\n<b>Requirement</b>\\n${String(x.requirement_text||\"\").slice(0,700)}\\n\\n<b>Draft</b>\\n${String(x.draft_answer||\"\").slice(0,1800)}`}}));"
      }
    },
    {
      "id": "00656c86ba98c0c4",
      "name": "Send Review to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -400,
        -80
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "SET_OWNER_TELEGRAM_CHAT_ID",
        "text": "={{ $json.review_message }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "✅ Approve",
                    "additionalFields": {
                      "callback_data": "={{ \"approve|\" + $json.requirement_id }}"
                    }
                  },
                  {
                    "text": "❌ Reject",
                    "additionalFields": {
                      "callback_data": "={{ \"reject|\" + $json.requirement_id }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "✏️ Request edit",
                    "additionalFields": {
                      "callback_data": "={{ \"edit|\" + $json.requirement_id }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      }
    },
    {
      "id": "19a675cc1b880c8e",
      "name": "Send Review to Owner Email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        -400,
        80
      ],
      "parameters": {
        "resource": "email",
        "operation": "send",
        "fromEmail": "Sales Evidence Control Room <SET_FROM_EMAIL>",
        "toEmail": "={{ $json.owner_email || 'SET_OWNER_EMAIL' }}",
        "subject": "={{ 'RFP review required — ' + $json.requirement_id }}",
        "emailFormat": "text",
        "text": "={{ $json.review_message.replace(/<[^>]+>/g, '') }}",
        "options": {
          "appendAttribution": false
        }
      }
    },
    {
      "id": "253b04d82ea7f3c0",
      "name": "Mark Review Notified",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -100,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const x=$(\"Select Unnotified Reviews\").item.json; return [{json:{...x,owner_notified_at:new Date().toISOString(),updated_at:new Date().toISOString()}}];"
      }
    },
    {
      "id": "9f4cb48f59a0e267",
      "name": "Update Review Queue Notification",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        170,
        0
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "documentId": {
          "mode": "id",
          "value": "SET_OPERATIONS_WORKBOOK_ID"
        },
        "sheetName": {
          "mode": "name",
          "value": "Review Queue"
        },
        "dataMode": "autoMapInputData",
        "columnToMatchOn": "requirement_id",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      },
      "alwaysOutputData": false
    },
    {
      "id": "cceb49bdfc900eeb",
      "name": "Decision listener",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1120,
        420
      ],
      "parameters": {
        "content": "## Decision endpoint\nConnect your Telegram Trigger here after creating the bot credential. Normalize callback data to `action|requirement_id`, re-read the row, reject stale/resolved actions, and write the final decision to both Review Queue and Audit Log.",
        "width": 840,
        "height": 270,
        "color": 6
      }
    },
    {
      "id": "fe75248494632023",
      "name": "Telegram Decision Trigger - Connect Credential",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.4,
      "position": [
        -920,
        760
      ],
      "parameters": {
        "updates": [
          "callback_query",
          "message"
        ],
        "additionalFields": {}
      }
    },
    {
      "id": "32819627e9a497c8",
      "name": "Normalize Review Decision",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -620,
        760
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const cb=$json.callback_query; if(!cb) return []; const [action,id]=String(cb.data||\"\").split(\"|\"); const allowed=[\"approve\",\"reject\",\"edit\"]; if(!allowed.includes(action)||!id) return [];\nreturn [{json:{requirement_id:id,action:action.toUpperCase(),actor_id:String(cb.from?.id||\"\"),telegram_chat_id:String(cb.message?.chat?.id||\"\"),telegram_message_id:cb.message?.message_id||\"\",callback_query_id:cb.id||\"\",decision_at:new Date().toISOString()}}];"
      }
    },
    {
      "id": "b64ebe6bb314c8e4",
      "name": "Read Review Queue for Decision",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -340,
        760
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SET_OPERATIONS_WORKBOOK_ID"
        },
        "sheetName": {
          "mode": "name",
          "value": "Review Queue"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      },
      "alwaysOutputData": true
    },
    {
      "id": "ed32d8b3bdb0e84a",
      "name": "Apply Idempotent Review Decision",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -60,
        760
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const e=$(\"Normalize Review Decision\").item.json; const row=$input.all().map(i=>i.json).find(x=>String(x.requirement_id)===e.requirement_id);\nif(!row||![\"PENDING_REVIEW\",\"EDIT_REQUESTED\"].includes(String(row.status).toUpperCase())) return [];\nconst status=e.action===\"APPROVE\"?\"APPROVED\":e.action===\"REJECT\"?\"REJECTED\":\"EDIT_REQUESTED\";\nreturn [{json:{...row,...e,status,decision:e.action,updated_at:new Date().toISOString(),event_id:`REV-${Date.now()}-${e.requirement_id}`,event_type:\"REVIEW_DECISION\",reason:row.control_reasons||\"\",evidence_ids:row.evidence_ids||\"\",created_at:new Date().toISOString()}}];"
      }
    },
    {
      "id": "0a42b6b30b1afec4",
      "name": "Upsert Review Decision",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        220,
        700
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "documentId": {
          "mode": "id",
          "value": "SET_OPERATIONS_WORKBOOK_ID"
        },
        "sheetName": {
          "mode": "name",
          "value": "Review Queue"
        },
        "dataMode": "autoMapInputData",
        "columnToMatchOn": "requirement_id",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      },
      "alwaysOutputData": false
    },
    {
      "id": "7f812fdb92ffc884",
      "name": "Append Review Audit",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        220,
        820
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "append",
        "documentId": {
          "mode": "id",
          "value": "SET_OPERATIONS_WORKBOOK_ID"
        },
        "sheetName": {
          "mode": "name",
          "value": "Audit Log"
        },
        "dataMode": "autoMapInputData",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      },
      "alwaysOutputData": false
    }
  ],
  "connections": {
    "Every 5 Minutes": {
      "main": [
        [
          {
            "node": "Read Pending Review Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Pending Review Queue": {
      "main": [
        [
          {
            "node": "Select Unnotified Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Unnotified Reviews": {
      "main": [
        [
          {
            "node": "Send Review to Telegram",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Review to Owner Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Review to Telegram": {
      "main": [
        [
          {
            "node": "Mark Review Notified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Review Notified": {
      "main": [
        [
          {
            "node": "Update Review Queue Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Decision Trigger - Connect Credential": {
      "main": [
        [
          {
            "node": "Normalize Review Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Review Decision": {
      "main": [
        [
          {
            "node": "Read Review Queue for Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Review Queue for Decision": {
      "main": [
        [
          {
            "node": "Apply Idempotent Review Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Idempotent Review Decision": {
      "main": [
        [
          {
            "node": "Upsert Review Decision",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append Review Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Africa/Lagos",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "executionTimeout": 600
  }
}
