{
  "id": "CODWF05ScheduledRecovery",
  "name": "COD · WF-05 Scheduled Recovery",
  "active": false,
  "nodes": [
    {
      "parameters": {
        "content": "## Layer 5 · Scheduled Recovery\nEvery two hours, evaluate stale pending and confirmed orders against Cairo quiet hours, reminder attempts, review age, and warehouse delay thresholds.",
        "height": 210,
        "width": 1400,
        "color": 3
      },
      "id": "wf05-note",
      "name": "Layer 5 · Recovery",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -320
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 */2 * * *"
            }
          ]
        }
      },
      "id": "wf05-schedule",
      "name": "Every 2 Hours Cairo Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -700,
        -40
      ]
    },
    {
      "parameters": {},
      "id": "wf05-manual",
      "name": "Manual Recovery Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -700,
        80
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "test-recovery",
        "authentication": "none",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "wf05-test",
      "name": "Test Recovery Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -700,
        200
      ],
      "webhookId": "cod-test-recovery-v1"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "wf05-enabled",
              "leftValue": "={{ $env.ENABLE_TEST_HOOKS }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "wf05-key",
              "leftValue": "={{ $json.headers['x-api-key'] || '' }}",
              "rightValue": "={{ $env.ORDER_WEBHOOK_KEY }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "wf05-gate",
      "name": "Recovery Test Hook Enabled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -480,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { accepted: true } }}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "wf05-accepted",
      "name": "Respond Recovery Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        -260,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { error: 'NOT_FOUND' } }}",
        "options": {
          "responseCode": 404
        }
      },
      "id": "wf05-disabled",
      "name": "Respond Recovery Unavailable",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        -260,
        420
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.INTERNAL_API_URL }}/api/internal/settings",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 10000
        }
      },
      "id": "wf05-settings",
      "name": "Fetch Recovery Settings",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -220,
        20
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "httpHeaderAuth": {
          "id": "cred_internal_api",
          "name": "COD Internal API Bearer"
        }
      }
    },
    {
      "parameters": {
        "url": "={{ $env.INTERNAL_API_URL }}/api/internal/orders/stale",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 10000
        }
      },
      "id": "wf05-stale",
      "name": "Query Stale Orders",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        20,
        20
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "httpHeaderAuth": {
          "id": "cred_internal_api",
          "name": "COD Internal API Bearer"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wf05-reminders",
              "name": "reminders",
              "value": "={{ $('Fetch Recovery Settings').item.json.reminders }}",
              "type": "object"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "wf05-attach",
      "name": "Attach Recovery Policy",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        20
      ]
    },
    {
      "parameters": {
        "jsCode": "let __smartCodRun;\n(() => {\n  // n8n/code/recovery.ts\n  function cairoMinutes(now) {\n    const parts = new Intl.DateTimeFormat(\"en-GB\", {\n      timeZone: \"Africa/Cairo\",\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      hourCycle: \"h23\"\n    }).formatToParts(now);\n    const hour = Number(parts.find((part) => part.type === \"hour\")?.value ?? 0);\n    const minute = Number(parts.find((part) => part.type === \"minute\")?.value ?? 0);\n    return hour * 60 + minute;\n  }\n  function timeToMinutes(value) {\n    const [hour, minute] = value.split(\":\").map(Number);\n    return hour * 60 + minute;\n  }\n  function isQuietHours(now, range) {\n    const [startValue, endValue] = range.split(\"-\");\n    const start = timeToMinutes(startValue);\n    const end = timeToMinutes(endValue);\n    const current = cairoMinutes(now);\n    return start <= end ? current >= start && current < end : current >= start || current < end;\n  }\n  function recoveryAction(input) {\n    const { status, ageHours, contactAttempts, settings, now } = input;\n    if (status === \"CONFIRMED\") return ageHours >= 24 ? \"WAREHOUSE_ALERT\" : \"NONE\";\n    if (status !== \"PENDING_CONFIRMATION\") return \"NONE\";\n    if (isQuietHours(now, settings.quietHours)) return \"WAIT_QUIET_HOURS\";\n    if (contactAttempts >= settings.maxAttempts) {\n      return ageHours >= settings.reviewAfterH ? \"ESCALATE_REVIEW\" : \"NONE\";\n    }\n    if (contactAttempts === 0 && ageHours >= settings.firstAfterH) return \"REMINDER_1\";\n    if (contactAttempts === 1 && ageHours >= settings.secondAfterH) return \"REMINDER_2\";\n    if (contactAttempts === 2 && ageHours >= settings.thirdAfterH) return \"REMINDER_3\";\n    return \"NONE\";\n  }\n  function run(items, context) {\n    const now = new Date(context?.now ?? Date.now());\n    return items.map((item) => ({\n      ...item,\n      json: {\n        ...item.json,\n        recoveryAction: recoveryAction({\n          status: String(item.json.status),\n          ageHours: Number(item.json.ageHours),\n          contactAttempts: Number(item.json.contactAttempts ?? 0),\n          settings: item.json.reminders,\n          now\n        })\n      }\n    }));\n  }\n\n  // recovery.bundle-entry.ts\n  __smartCodRun = run;\n})();\nreturn __smartCodRun($input.all(), { now: new Date().toISOString() });"
      },
      "id": "wf05-evaluate",
      "name": "Evaluate Order Age & Recovery Action",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        20
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "wf05-reminder",
                    "leftValue": "={{ $json.recoveryAction }}",
                    "rightValue": "^REMINDER_[123]$",
                    "operator": {
                      "type": "string",
                      "operation": "regex"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Reminder"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "wf05-review",
                    "leftValue": "={{ $json.recoveryAction }}",
                    "rightValue": "ESCALATE_REVIEW",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Review"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "wf05-warehouse",
                    "leftValue": "={{ $json.recoveryAction }}",
                    "rightValue": "WAREHOUSE_ALERT",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Warehouse"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "id": "wf05-router",
      "name": "Recovery Action Router",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        740,
        20
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wf05-r-audience",
              "name": "audience",
              "value": "customer",
              "type": "string"
            },
            {
              "id": "wf05-r-channel",
              "name": "channel",
              "value": "auto",
              "type": "string"
            },
            {
              "id": "wf05-r-template",
              "name": "template",
              "value": "={{ $json.recoveryAction.toLowerCase() }}",
              "type": "string"
            },
            {
              "id": "wf05-r-language",
              "name": "language",
              "value": "ar",
              "type": "string"
            },
            {
              "id": "wf05-r-order",
              "name": "orderId",
              "value": "={{ $json.id }}",
              "type": "string"
            },
            {
              "id": "wf05-r-chat",
              "name": "chatId",
              "value": "={{ $json.customer.telegramChatId || '' }}",
              "type": "string"
            },
            {
              "id": "wf05-r-email",
              "name": "email",
              "value": "={{ $json.customer.email || '' }}",
              "type": "string"
            },
            {
              "id": "wf05-r-vars",
              "name": "vars",
              "value": "={{ { orderId: $json.id, confirmationUrl: $env.APP_PUBLIC_URL + '/c/' + $json.confirmationToken } }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "wf05-reminder-payload",
      "name": "Prepare Reminder N",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1000,
        -100
      ]
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "CODWF00SendNotification",
          "mode": "id"
        },
        "mode": "once",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "id": "wf05-reminder-send",
      "name": "Send Recovery Reminder",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        1240,
        -100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.INTERNAL_API_URL }}/api/internal/orders/{{ $('Evaluate Order Age & Recovery Action').item.json.id }}/contact-attempt",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ actor: 'n8n' }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "wf05-attempt",
      "name": "Log Reminder Contact Attempt",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1480,
        -100
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "httpHeaderAuth": {
          "id": "cred_internal_api",
          "name": "COD Internal API Bearer"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.INTERNAL_API_URL }}/api/internal/orders/{{ $json.id }}/transition",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ to: 'NEEDS_REVIEW', reason: 'CONTACT_ATTEMPTS_EXHAUSTED', actor: 'n8n' }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "wf05-review-transition",
      "name": "Escalate Exhausted Order to Review",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1000,
        40
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "httpHeaderAuth": {
          "id": "cred_internal_api",
          "name": "COD Internal API Bearer"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wf05-e-audience",
              "name": "audience",
              "value": "admin",
              "type": "string"
            },
            {
              "id": "wf05-e-channel",
              "name": "channel",
              "value": "auto",
              "type": "string"
            },
            {
              "id": "wf05-e-template",
              "name": "template",
              "value": "review_alert",
              "type": "string"
            },
            {
              "id": "wf05-e-language",
              "name": "language",
              "value": "ar",
              "type": "string"
            },
            {
              "id": "wf05-e-vars",
              "name": "vars",
              "value": "={{ { orderId: $('Evaluate Order Age & Recovery Action').item.json.id, riskScore: $('Evaluate Order Age & Recovery Action').item.json.riskScore } }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "wf05-review-payload",
      "name": "Prepare Recovery Review Alert",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1240,
        40
      ]
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "CODWF00SendNotification",
          "mode": "id"
        },
        "mode": "once",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "id": "wf05-review-send",
      "name": "Send Recovery Review Alert",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        1480,
        40
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wf05-w-audience",
              "name": "audience",
              "value": "admin",
              "type": "string"
            },
            {
              "id": "wf05-w-channel",
              "name": "channel",
              "value": "auto",
              "type": "string"
            },
            {
              "id": "wf05-w-template",
              "name": "template",
              "value": "warehouse_packing_slip",
              "type": "string"
            },
            {
              "id": "wf05-w-language",
              "name": "language",
              "value": "ar",
              "type": "string"
            },
            {
              "id": "wf05-w-vars",
              "name": "vars",
              "value": "={{ { orderId: $json.id, items: $json.items.map(item => item.sku + ' × ' + item.qty).join('، '), labelUrl: $env.APP_PUBLIC_URL + '/awb/' + $json.id } }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "wf05-warehouse-payload",
      "name": "Prepare Delayed Warehouse Alert",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1000,
        180
      ]
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "CODWF00SendNotification",
          "mode": "id"
        },
        "mode": "once",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "id": "wf05-warehouse-send",
      "name": "Send Delayed Warehouse Alert",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        1240,
        180
      ]
    }
  ],
  "connections": {
    "Every 2 Hours Cairo Trigger": {
      "main": [
        [
          {
            "node": "Fetch Recovery Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Recovery Trigger": {
      "main": [
        [
          {
            "node": "Fetch Recovery Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test Recovery Webhook": {
      "main": [
        [
          {
            "node": "Recovery Test Hook Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recovery Test Hook Enabled?": {
      "main": [
        [
          {
            "node": "Fetch Recovery Settings",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond Recovery Accepted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Recovery Unavailable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Recovery Settings": {
      "main": [
        [
          {
            "node": "Query Stale Orders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Stale Orders": {
      "main": [
        [
          {
            "node": "Attach Recovery Policy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Recovery Policy": {
      "main": [
        [
          {
            "node": "Evaluate Order Age & Recovery Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Order Age & Recovery Action": {
      "main": [
        [
          {
            "node": "Recovery Action Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recovery Action Router": {
      "main": [
        [
          {
            "node": "Prepare Reminder N",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Escalate Exhausted Order to Review",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Delayed Warehouse Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Reminder N": {
      "main": [
        [
          {
            "node": "Send Recovery Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Recovery Reminder": {
      "main": [
        [
          {
            "node": "Log Reminder Contact Attempt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Escalate Exhausted Order to Review": {
      "main": [
        [
          {
            "node": "Prepare Recovery Review Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Recovery Review Alert": {
      "main": [
        [
          {
            "node": "Send Recovery Review Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Delayed Warehouse Alert": {
      "main": [
        [
          {
            "node": "Send Delayed Warehouse Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "errorWorkflow": "CODWF99ErrorHandler",
    "timezone": "Africa/Cairo"
  },
  "versionId": "f0500000-0000-4000-8000-000000000000",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "tags": []
}
