{
  "nodes": [
    {
      "id": "30659302-22f3-4470-bf2f-2d8c80ae16bd",
      "name": "Main Sticky",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        64
      ],
      "parameters": {
        "color": 2,
        "width": 500,
        "height": 456,
        "content": "## WhatsApp Career Ranking Bot\nAnalyze resumes via WhatsApp to provide career scores, ATS feedback, and ranking insights.\n\n\n### How it works\n1. User triggers bot via keyword.\n\n2. Collects name, target role, and PDF resume.\n\n3. Scoring Engine calculates metrics.\n\n4. AI identifies rejection reasons.\n\n5. Returns personalized report to user."
      },
      "typeVersion": 1
    },
    {
      "id": "d89d7440-ad39-4ec5-aff8-2b9ba2dd72da",
      "name": "WhatsApp Trigger",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        -1328,
        256
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "typeVersion": 1
    },
    {
      "id": "41605855-506a-4e80-a14c-be618903af45",
      "name": "Conversation State Manager",
      "type": "n8n-nodes-base.code",
      "position": [
        -1104,
        256
      ],
      "parameters": {
        "jsCode": "// ============================================\n// CONVERSATION STATE MANAGER \u2014 PRODUCTION\n// All fields always saved unconditionally\n// ============================================\n\nconst SUPABASE_URL = 'https://zfyyiewkkjyzxnyiabvk.supabase.co';\nconst SUPABASE_KEY = 'eyJ_YOUR_JWT_TOKEN_HERE';\n\nconst readHeaders = {\n  'apikey': SUPABASE_KEY,\n  'Authorization': `Bearer ${SUPABASE_KEY}`,\n  'Content-Type': 'application/json'\n};\n\nconst upsertHeaders = {\n  'apikey': SUPABASE_KEY,\n  'Authorization': `Bearer ${SUPABASE_KEY}`,\n  'Content-Type': 'application/json',\n  'Prefer': 'resolution=merge-duplicates,return=minimal'\n};\n\nconst message = $input.first().json;\n\nif (!message.messages || !message.messages[0]) {\n  return [{ json: { action: 'IGNORE' } }];\n}\n\nconst msg = message.messages[0];\n\nif (msg.type === 'status' || (!msg.text && !msg.document)) {\n  return [{ json: { action: 'IGNORE' } }];\n}\n\nconst from = msg.from;\nconst hasText = !!msg.text;\nconst hasDocument = !!msg.document;\nconst userText = hasText ? msg.text.body.trim() : '';\nconst upperText = userText.toUpperCase().replace(/[^A-Z0-9 ]/g, '').trim();\n\n// Extract WhatsApp profile name from contacts array\nlet profileName = '';\ntry {\n  if (message.contacts && message.contacts[0] && message.contacts[0].profile) {\n    profileName = message.contacts[0].profile.name || '';\n  }\n} catch(e) { /* no contact info available */ }\n\n// \u2500\u2500 LOAD SESSION FROM SUPABASE \u2500\u2500\nlet session = { state: 'IDLE', name: '', target_role: '', started_at: new Date().toISOString() };\n\ntry {\n  const rows = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?phone=eq.${encodeURIComponent(from)}&select=*`,\n    headers: readHeaders,\n    json: true\n  });\n  if (Array.isArray(rows) && rows.length > 0) {\n    session = rows[0];\n    session.name = session.name || profileName || '';\n    session.target_role = session.target_role || '';\n  }\n} catch(e) {\n  console.error('Supabase read error:', e.message);\n}\n\n// Always update name from WhatsApp profile if not yet saved\nif (!session.name && profileName) {\n  session.name = profileName;\n}\n\nlet action = 'IGNORE';\nlet replyText = '';\n\n// \u2500\u2500 STATE MACHINE \u2500\u2500\nswitch (session.state) {\n\n  case 'IDLE':\n    if (\n      upperText === 'CHECK MY RANK' ||\n      upperText === 'CHECK' ||\n      upperText === 'CHECKMYRANK' ||\n      upperText === 'START' ||\n      upperText === 'RANK'\n    ) {\n      action = 'SEND_INTRO';\n      session.state = 'WAITING_YES';\n      session.started_at = new Date().toISOString();\n      replyText = `*Career Ranking Engine* \ud83d\udc4b\\n\\nI'll analyze your resume and show you:\\n\\n\ud83d\udcca Your *career score* (0-100)\\n\u274c *Why you're getting rejected*\\n\u2705 *What to fix* to get more interviews\\n\\nThis takes about 60 seconds.\\n\\n\ud83d\udc49 *Type YES to start*`;\n    } else {\n      action = 'SEND_HELP';\n      session.started_at = new Date().toISOString();\n      replyText = `\ud83d\udc4b Want to know where you stand in the job market?\\n\\n\ud83d\udcf2 Type *CHECK MY RANK* to get started!`;\n    }\n    break;\n\n  case 'WAITING_YES':\n    if (\n      upperText === 'YES' ||\n      upperText === 'Y' ||\n      upperText === 'YEAH' ||\n      upperText === 'YEP' ||\n      upperText === 'SURE' ||\n      upperText === 'OK' ||\n      upperText === 'OKAY'\n    ) {\n      action = 'ASK_NAME';\n      session.state = 'WAITING_NAME';\n      replyText = `Great! \ud83d\ude80\\n\\n*Step 1 of 3*\\n\\nWhat's your *full name*?\\n\\n_Example: Rahul Sharma_`;\n    } else if (upperText === 'NO' || upperText === 'N' || upperText === 'CANCEL') {\n      action = 'SEND_REPLY';\n      session.state = 'IDLE';\n      replyText = `No worries! Whenever you're ready, type *CHECK MY RANK* \ud83d\udcaa`;\n    } else if (\n      upperText === 'CHECK MY RANK' ||\n      upperText === 'CHECK' ||\n      upperText === 'START'\n    ) {\n      action = 'SEND_INTRO';\n      session.state = 'WAITING_YES';\n      session.started_at = new Date().toISOString();\n      replyText = `*Career Ranking Engine* \ud83d\udc4b\\n\\nI'll analyze your resume and show you:\\n\\n\ud83d\udcca Your *career score* (0-100)\\n\u274c *Why you're getting rejected*\\n\u2705 *What to fix* to get more interviews\\n\\nThis takes about 60 seconds.\\n\\n\ud83d\udc49 *Type YES to start*`;\n    } else {\n      action = 'SEND_REPLY';\n      replyText = `Please type *YES* to start or *NO* to cancel.`;\n    }\n    break;\n\n  case 'WAITING_NAME':\n    if (hasText && userText.length >= 2 && userText.length <= 100) {\n      action = 'ASK_ROLE';\n      session.name = userText;\n      session.state = 'WAITING_ROLE';\n      replyText = `Nice to meet you, *${session.name}*! \ud83d\udc4b\\n\\n*Step 2 of 3*\\n\\nWhich *job role* are you targeting?\\n\\n_Example: Data Analyst, Frontend Developer, Product Manager_`;\n    } else {\n      action = 'SEND_REPLY';\n      replyText = `Please enter your full name (e.g. \"Rahul Sharma\"):`;\n    }\n    break;\n\n  case 'WAITING_ROLE':\n    if (hasText && userText.length >= 2 && userText.length <= 150) {\n      action = 'ASK_RESUME';\n      session.target_role = userText;\n      session.state = 'WAITING_RESUME';\n      replyText = `Got it \u2014 *${session.target_role}* \ud83c\udfaf\\n\\n*Step 3 of 3*\\n\\nUpload your *resume as a PDF* \ud83d\udcce\\n\\n_(Tap \ud83d\udcce and attach your PDF)_`;\n    } else {\n      action = 'SEND_REPLY';\n      replyText = `Please enter a valid job role (e.g. \"Data Analyst\", \"Software Engineer\"):`;\n    }\n    break;\n\n  case 'WAITING_RESUME':\n    if (hasDocument) {\n      if (msg.document.mime_type === 'application/pdf') {\n        action = 'PROCESS_RESUME';\n        session.state = 'PROCESSING';\n        replyText = `\ud83d\udcc4 Resume received!\\n\\n\u23f3 *Analyzing your profile for ${session.target_role}...*\\n\\nHang tight, this takes 15-30 seconds! \ud83d\udd0d`;\n      } else {\n        action = 'SEND_REPLY';\n        replyText = `\u26a0\ufe0f Please upload a *PDF file* only. DOC/DOCX not supported.`;\n      }\n    } else {\n      action = 'SEND_REPLY';\n      replyText = `Please tap \ud83d\udcce and attach your resume as a *PDF file*.`;\n    }\n    break;\n\n  case 'PROCESSING':\n    action = 'SEND_REPLY';\n    replyText = `\u23f3 Still analyzing... please wait a moment!`;\n    break;\n\n  default:\n    session.state = 'IDLE';\n    session.started_at = new Date().toISOString();\n    action = 'SEND_HELP';\n    replyText = `Type *CHECK MY RANK* to get started!`;\n}\n\n// \u2500\u2500 SAVE SESSION \u2014 always send ALL fields \u2500\u2500\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?on_conflict=phone`,\n    headers: upsertHeaders,\n    body: {\n      phone: from,\n      state: session.state,\n      name: session.name || '',\n      target_role: session.target_role || '',\n      started_at: session.started_at || new Date().toISOString(),\n      updated_at: new Date().toISOString()\n    },\n    json: true\n  });\n} catch(e) {\n  console.error('Supabase save error:', e.message);\n}\n\nreturn [{\n  json: {\n    action,\n    replyText,\n    from,\n    session,\n    documentId: hasDocument ? msg.document.id : null,\n    messageType: hasDocument ? 'document' : 'text'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a0388610-aa7a-4006-9b5f-ed2ec01b3152",
      "name": "Route Action",
      "type": "n8n-nodes-base.switch",
      "position": [
        -880,
        256
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Process Resume",
              "conditions": {
                "options": {
                  "version": 2,
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "route-process",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "PROCESS_RESUME"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Send Reply",
              "conditions": {
                "options": {
                  "version": 2,
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "route-reply",
                    "operator": {
                      "type": "string",
                      "operation": "notEquals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "PROCESS_RESUME"
                  },
                  {
                    "id": "route-not-ignore",
                    "operator": {
                      "type": "string",
                      "operation": "notEquals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "IGNORE"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "dc85e1fc-1209-4e3c-beb6-94a6b7eed92e",
      "name": "Send Conversation Reply",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        -656,
        352
      ],
      "parameters": {
        "textBody": "={{ $json.replyText }}",
        "operation": "send",
        "phoneNumberId": "YOUR_PHONE_NUMBER_HERE",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $json.from }}"
      },
      "typeVersion": 1
    },
    {
      "id": "43c95c4c-b314-480b-b3a9-3d0a9afc2a1a",
      "name": "Send Processing Message",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        -656,
        160
      ],
      "parameters": {
        "textBody": "={{ $json.replyText }}",
        "operation": "send",
        "phoneNumberId": "YOUR_PHONE_NUMBER_HERE",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $json.from }}"
      },
      "typeVersion": 1
    },
    {
      "id": "101eea2c-b3da-4142-8aac-55298f7d81de",
      "name": "Get Resume URL",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        -432,
        160
      ],
      "parameters": {
        "resource": "media",
        "operation": "mediaUrlGet",
        "mediaGetId": "={{ $('Conversation State Manager').item.json.documentId }}"
      },
      "typeVersion": 1
    },
    {
      "id": "cb5b5d8f-449b-48e3-a41e-c9f1ca9a9b69",
      "name": "Download Resume",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -208,
        160
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "a66ac75a-6a17-47c2-b588-e863c12648b8",
      "name": "Extract Resume Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        64,
        160
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1
    },
    {
      "id": "638a5b48-bc0c-4c47-a0f1-79726cb1061f",
      "name": "Set Resume Data",
      "type": "n8n-nodes-base.set",
      "position": [
        288,
        160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "set-resume-text",
              "name": "resume_text",
              "type": "string",
              "value": "={{ $json.text }}"
            },
            {
              "id": "set-target-role",
              "name": "target_role",
              "type": "string",
              "value": "={{ $('Conversation State Manager').item.json.session.target_role }}"
            },
            {
              "id": "set-user-from",
              "name": "from",
              "type": "string",
              "value": "={{ $('Conversation State Manager').item.json.from }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cff64e41-74ec-4491-93df-152b5bdd4de6",
      "name": "Scoring Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        160
      ],
      "parameters": {
        "jsCode": "// ============================================\n// REAL SCORING ENGINE \u2014 5 Dimensions = 100pts\n// Impact(25) + Relevance(25) + ATS(20) + Clarity(15) + Completeness(15)\n// NO caps. NO random. NO fake data.\n// ============================================\n\nconst SUPABASE_URL = 'https://zfyyiewkkjyzxnyiabvk.supabase.co';\nconst SUPABASE_KEY = 'eyJ_YOUR_JWT_TOKEN_HERE';\n\nconst readHeaders = {\n  'apikey': SUPABASE_KEY,\n  'Authorization': `Bearer ${SUPABASE_KEY}`,\n  'Content-Type': 'application/json'\n};\nconst upsertHeaders = { ...readHeaders, 'Prefer': 'resolution=merge-duplicates,return=minimal' };\n\nconst resumeText = $input.first().json.resume_text || '';\nconst targetRole = ($input.first().json.target_role || '').toLowerCase().trim();\nconst from = $input.first().json.from || '';\nconst text = resumeText.toLowerCase();\nconst words = text.split(/\\s+/).filter(w => w.length > 0);\nconst wordCount = words.length;\n\n// 1. IMPACT SCORE (0-25)\nlet impactScore = 0;\nconst impactVerbs = [\n  'led','built','managed','created','developed','launched','grew','increased',\n  'reduced','improved','achieved','implemented','delivered','designed','scaled',\n  'optimized','automated','generated','drove','spearheaded','founded','pioneered',\n  'established','transformed','streamlined','negotiated','secured','acquired',\n  'deployed','migrated','published','awarded','trained','mentored','directed',\n  'accelerated','boosted','cut','saved','shipped','refactored','architected'\n];\nconst uniqueVerbsUsed = [...new Set(impactVerbs.filter(v => text.includes(v)))];\nimpactScore += Math.min(uniqueVerbsUsed.length * 1.3, 12);\nconst quantPattern = /\\d+[%+xk]|\\$\\d+|\\d+\\s*(million|billion|thousand|k\\b|users|customers|clients|revenue|growth|reduction|increase|improvement|efficiency|saving|profit|leads|conversions|downloads)/gi;\nconst quantMatches = (resumeText.match(quantPattern) || []).length;\nimpactScore += Math.min(quantMatches * 2.5, 13);\nimpactScore = Math.min(Math.round(impactScore), 25);\n\n// 2. RELEVANCE SCORE (0-25)\nlet relevanceScore = 0;\nconst roleKeywordsMap = {\n  'data analyst': ['sql','python','excel','tableau','power bi','data','analytics','statistics','visualization','dashboard','reporting','pandas','numpy','etl','bigquery','snowflake','looker'],\n  'software engineer': ['javascript','python','java','react','node','api','git','aws','docker','agile','rest','sql','typescript','ci/cd','microservices','spring','kafka'],\n  'frontend developer': ['html','css','javascript','react','vue','angular','typescript','responsive','ui','ux','figma','tailwind','next.js','sass','webpack','vite','redux'],\n  'backend developer': ['python','java','node','api','database','sql','nosql','aws','docker','microservices','rest','graphql','redis','kafka','postgresql','mongodb','spring'],\n  'full stack developer': ['javascript','react','node','python','sql','api','git','docker','aws','typescript','mongodb','postgresql','graphql','rest'],\n  'product manager': ['product','roadmap','stakeholder','agile','scrum','user research','a/b testing','kpi','metrics','strategy','prioritization','jira','okr','gtm','sprint','analytics'],\n  'marketing manager': ['marketing','campaign','seo','social media','content','brand','analytics','roi','digital','engagement','conversion','google ads','email marketing','crm','hubspot'],\n  'ux designer': ['ux','ui','figma','sketch','wireframe','prototype','user research','usability','design thinking','interaction design','accessibility','user testing','adobe xd'],\n  'devops engineer': ['docker','kubernetes','ci/cd','aws','terraform','jenkins','ansible','linux','monitoring','pipeline','cloud','infrastructure','helm','prometheus','grafana'],\n  'machine learning engineer': ['python','tensorflow','pytorch','ml','deep learning','neural network','nlp','model','training','scikit-learn','feature engineering','mlops','llm','huggingface'],\n  'data scientist': ['python','r','machine learning','statistics','pandas','scikit-learn','tensorflow','sql','visualization','hypothesis','regression','classification','clustering','nlp'],\n  'business analyst': ['business analysis','requirements','stakeholder','sql','process','documentation','jira','agile','reporting','data','workflow','use case','gap analysis'],\n  'cloud engineer': ['aws','azure','gcp','terraform','kubernetes','docker','ci/cd','s3','ec2','lambda','cloudformation','iam','vpc'],\n  'android developer': ['android','kotlin','java','xml','jetpack','firebase','mvvm','retrofit','room','gradle','material design','coroutines'],\n  'ios developer': ['ios','swift','xcode','uikit','swiftui','firebase','alamofire','mvvm','core data','objective-c'],\n  'project manager': ['project management','pmp','agile','scrum','waterfall','budget','stakeholder','risk','timeline','deliverables','jira','ms project','gantt'],\n  'cybersecurity': ['security','penetration testing','firewall','siem','soc','vulnerability','encryption','compliance','iso27001','owasp','incident response','threat']\n};\n\nlet matchedKeywords = [];\nlet bestRoleMatch = null;\n\nfor (const [role, keywords] of Object.entries(roleKeywordsMap)) {\n  if (targetRole.includes(role) || role.includes(targetRole)) {\n    matchedKeywords = keywords; bestRoleMatch = role; break;\n  }\n}\nif (!bestRoleMatch) {\n  let bestOverlap = 0;\n  const targetWords = targetRole.split(' ');\n  for (const [role, keywords] of Object.entries(roleKeywordsMap)) {\n    const overlap = role.split(' ').filter(w => targetWords.some(tw => tw.includes(w) || w.includes(tw))).length;\n    if (overlap > bestOverlap) { bestOverlap = overlap; matchedKeywords = keywords; bestRoleMatch = role; }\n  }\n}\nif (matchedKeywords.length === 0) {\n  matchedKeywords = ['experience','team','project','management','communication','leadership','skills','results','strategy','analysis'];\n}\nconst keywordHits = matchedKeywords.filter(kw => text.includes(kw)).length;\nrelevanceScore = Math.round((keywordHits / matchedKeywords.length) * 25);\n\n// 3. ATS READINESS SCORE (0-20)\nlet atsScore = 0;\nconst sections = ['experience','education','skills','summary','objective','projects','certifications','achievements','work history','accomplishments','awards','publications'];\natsScore += Math.min(sections.filter(s => text.includes(s)).length * 2, 8);\nif (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/.test(resumeText)) atsScore += 3;\nif (/\\+?\\d[\\d\\s\\-().]{7,}/.test(resumeText)) atsScore += 2;\nif (/linkedin\\.com\\/in\\//i.test(resumeText)) atsScore += 2;\nif (/github\\.com\\//i.test(resumeText)) atsScore += 2;\nconst specialDensity = (resumeText.match(/[^\\x00-\\x7F]/g) || []).length / Math.max(resumeText.length, 1);\nif (specialDensity < 0.005) atsScore += 3; else if (specialDensity < 0.02) atsScore += 1;\natsScore = Math.min(atsScore, 20);\n\n// 4. CLARITY SCORE (0-15)\nlet clarityScore = 0;\nconst sentences = resumeText.split(/[.!?]+/).filter(s => s.trim().length > 5);\nconst avgSentLen = sentences.length > 0 ? wordCount / sentences.length : 0;\nif (avgSentLen >= 8 && avgSentLen <= 20) clarityScore += 5; else if (avgSentLen > 0) clarityScore += 2;\nconst bulletCount = (resumeText.match(/[\u2022\\-\\*]/g) || []).length;\nif (bulletCount >= 10) clarityScore += 5; else if (bulletCount >= 5) clarityScore += 3; else if (bulletCount >= 2) clarityScore += 1;\nconst yearRanges = (resumeText.match(/20\\d{2}\\s*[-to]+\\s*(20\\d{2}|present|current)/gi) || []).length;\nconst yearMentions = (resumeText.match(/20[0-2]\\d/g) || []).length;\nif (yearRanges >= 2) clarityScore += 5; else if (yearMentions >= 3) clarityScore += 3; else if (yearMentions >= 1) clarityScore += 1;\nclarityScore = Math.min(clarityScore, 15);\n\n// 5. COMPLETENESS SCORE (0-15)\nlet completenessScore = 0;\nif (wordCount >= 300 && wordCount <= 700) completenessScore += 6;\nelse if (wordCount >= 200 && wordCount <= 900) completenessScore += 4;\nelse if (wordCount >= 100) completenessScore += 2;\nif (/bachelor|master|phd|b\\.sc|m\\.sc|b\\.tech|m\\.tech|bca|mca|degree|university|college/i.test(resumeText)) completenessScore += 4;\nif (/certif|certified|aws certified|google cloud|azure|pmp|cpa|cfa|cissp|comptia|coursera|udemy/i.test(resumeText)) completenessScore += 3;\nif (/project/i.test(resumeText)) completenessScore += 2;\ncompletenessScore = Math.min(completenessScore, 15);\n\n// FINAL SCORE \u2014 true 0-100, no cap, no randomness\nconst finalScore = impactScore + relevanceScore + atsScore + clarityScore + completenessScore;\nconst normalizedRole = bestRoleMatch || targetRole;\n\n// SAVE SCORE TO SUPABASE\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: `${SUPABASE_URL}/rest/v1/resume_scores?on_conflict=phone,target_role`,\n    headers: upsertHeaders,\n    body: { phone: from, target_role: normalizedRole, score: finalScore, scored_at: new Date().toISOString() },\n    json: true\n  });\n} catch(e) { console.error('Score save error:', e.message); }\n\n// FETCH REAL CANDIDATE STATS FROM SUPABASE\nlet totalCandidates = 1, rank = 1, percentile = 'First submission!', percentileLabel = '\ud83c\udf1f';\ntry {\n  const allScores = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `${SUPABASE_URL}/rest/v1/resume_scores?target_role=eq.${encodeURIComponent(normalizedRole)}&select=score&order=score.desc`,\n    headers: readHeaders,\n    json: true\n  });\n  if (Array.isArray(allScores) && allScores.length > 1) {\n    totalCandidates = allScores.length;\n    const sorted = allScores.map(r => r.score).sort((a, b) => b - a);\n    rank = sorted.findIndex(s => s <= finalScore) + 1;\n    if (rank <= 0) rank = totalCandidates;\n    const belowCount = allScores.filter(r => r.score < finalScore).length;\n    const pct = Math.round((belowCount / totalCandidates) * 100);\n    if (pct >= 85)      { percentile = 'Top 15%';    percentileLabel = '\ud83d\udfe2'; }\n    else if (pct >= 70) { percentile = 'Top 30%';    percentileLabel = '\ud83d\udfe1'; }\n    else if (pct >= 50) { percentile = 'Top 50%';    percentileLabel = '\ud83d\udfe0'; }\n    else if (pct >= 35) { percentile = 'Top 65%';    percentileLabel = '\ud83d\udd34'; }\n    else                { percentile = 'Bottom 35%'; percentileLabel = '\ud83d\udd34'; }\n  }\n} catch(e) { console.error('Stats fetch error:', e.message); }\n\n// INTERVIEW PROBABILITY \u2014 honest, can reach High\nlet probability;\nif (finalScore >= 80)      probability = 'High';\nelse if (finalScore >= 65) probability = 'Moderate';\nelse if (finalScore >= 50) probability = 'Low';\nelse                       probability = 'Very Low';\n\n// EXTRACT EMAIL FROM RESUME\nlet extractedEmail = '';\nconst emailMatch = resumeText.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/);\nif (emailMatch) extractedEmail = emailMatch[0];\n\n// EXTRACT LINKEDIN URL FROM RESUME\nlet extractedLinkedin = '';\nconst linkedinMatch = resumeText.match(/(?:https?:\\/\\/)?(?:www\\.)?linkedin\\.com\\/in\\/[a-zA-Z0-9_\\-]+\\/?/i);\nif (linkedinMatch) {\n  extractedLinkedin = linkedinMatch[0];\n  if (!extractedLinkedin.startsWith('http')) extractedLinkedin = 'https://' + extractedLinkedin;\n}\n\n// SAVE EMAIL, LINKEDIN, RESUME TEXT TO WHATSAPP_SESSIONS\ntry {\n  await this.helpers.httpRequest({\n    method: 'PATCH',\n    url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?phone=eq.${encodeURIComponent(from)}`,\n    headers: { ...readHeaders, 'Prefer': 'return=minimal' },\n    body: {\n      email: extractedEmail,\n      linkedin_url: extractedLinkedin,\n      resume_url: ''\n    },\n    json: true\n  });\n} catch(e) { console.error('Email/LinkedIn/Resume save error:', e.message); }\n\nreturn [{\n  json: {\n    finalScore, percentile, percentileLabel, rank, totalCandidates, probability,\n    impactScore, relevanceScore, atsScore, clarityScore, completenessScore,\n    bestRoleMatch: normalizedRole, wordCount, extractedEmail, extractedLinkedin,\n    targetRole: $input.first().json.target_role, from, resume_text: resumeText\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9d78444a-c2bc-4e3c-8375-cc69a4576c6a",
      "name": "OpenAI - Rejection Analysis",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        512,
        256
      ],
      "parameters": {
        "url": "YOUR_API_ENDPOINT_HERE",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ {\n  \"model\": \"gpt-4o-mini\",\n  \"max_tokens\": 800,\n  \"response_format\": { \"type\": \"json_object\" },\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are an expert ATS resume reviewer. Return ONLY valid JSON, no extra text.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Analyze this resume for the role of \" + $json.target_role + \".\\n\\nResume:\\n\" + $json.resume_text.substring(0, 3000) + \"\\n\\nReturn ONLY this JSON:\\n{\\n  \\\"rejection_reasons\\\": [\\n    { \\\"reason\\\": \\\"specific reason\\\" },\\n    { \\\"reason\\\": \\\"second reason\\\" },\\n    { \\\"reason\\\": \\\"third reason\\\" }\\n  ],\\n  \\\"fixes\\\": [\\n    { \\\"fix\\\": \\\"specific fix\\\" },\\n    { \\\"fix\\\": \\\"second fix\\\" },\\n    { \\\"fix\\\": \\\"third fix\\\" }\\n  ],\\n  \\\"one_liner\\\": \\\"one sentence summary of biggest weakness\\\"\\n}\"\n    }\n  ]\n} }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "1f4dda0f-9866-4f94-8a65-cfd27aa081e6",
      "name": "Parse AI Response",
      "type": "n8n-nodes-base.set",
      "position": [
        736,
        256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "parse-ai",
              "name": "ai_analysis",
              "type": "object",
              "value": "={{ JSON.parse($json.choices[0].message.content) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6eab4bb5-1fbc-4e56-b146-55054ec3cca8",
      "name": "Merge Score + AI",
      "type": "n8n-nodes-base.merge",
      "position": [
        928,
        176
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3
    },
    {
      "id": "ce82879b-2bce-44c9-87d6-a66aca4b609e",
      "name": "Format Result Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        176
      ],
      "parameters": {
        "jsCode": "// FORMAT FINAL MESSAGE \u2014 PRODUCTION\n// No mock/fallback results. No share footer. No name.\n\nconst SUPABASE_URL = 'https://zfyyiewkkjyzxnyiabvk.supabase.co';\nconst SUPABASE_KEY = 'eyJ_YOUR_JWT_TOKEN_HERE';\n\nconst item = $input.first().json;\n\nconst score = item.finalScore;\nconst percentile = item.percentile;\nconst percentileLabel = item.percentileLabel;\nconst rank = item.rank;\nconst total = item.totalCandidates;\nconst probability = item.probability;\nconst targetRole = item.targetRole;\nconst ai = item.ai_analysis;\n\n// Score bar\nconst filled = Math.round(score / 10);\nconst empty = 10 - filled;\nconst scoreBar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);\n\n// Rejection reasons \u2014 real AI output only, no fallback\nlet rejectionBlock = '';\nai.rejection_reasons.slice(0, 3).forEach((r, i) => {\n  rejectionBlock += `${i + 1}. ${r.reason}\\n`;\n});\n\n// Fixes \u2014 real AI output only, no fallback\nlet fixesBlock = '';\nai.fixes.slice(0, 3).forEach((f, i) => {\n  fixesBlock += `${i + 1}. ${f.fix}\\n`;\n});\n\nconst oneLiner = ai.one_liner;\n\nconst message = `*CAREER RANK REPORT*\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n\ud83c\udfaf Role: *${targetRole}*\n\n\ud83d\udcca *Your Score: ${score}/100*\n${scoreBar}\n\n\ud83d\udcc8 Percentile: ${percentileLabel} *${percentile}*\n\ud83c\udfc5 Rank: *#${rank}* out of ${total}+ candidates\n\u26a1 Interview Probability: *${probability}*\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\u274c *WHY YOU'RE GETTING REJECTED:*\n\n${rejectionBlock}\n\ud83d\udca1 _${oneLiner}_\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\u2705 *WHAT TO FIX:*\n\n${fixesBlock}\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\ud83d\ude80 *Want to improve your rank?*\n\nGet personalized job recommendations \ud83d\udc47\n\ud83d\udd17 https://meracareer.io`;\n\n// Reset session to IDLE \u2014 preserve name, email, linkedin, resume\ntry {\n  await this.helpers.httpRequest({\n    method: 'PATCH',\n    url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?phone=eq.${encodeURIComponent(item.from)}`,\n    headers: {\n      'apikey': SUPABASE_KEY,\n      'Authorization': `Bearer ${SUPABASE_KEY}`,\n      'Content-Type': 'application/json',\n      'Prefer': 'return=minimal'\n    },\n    body: {\n      state: 'IDLE',\n      updated_at: new Date().toISOString()\n    },\n    json: true\n  });\n} catch(e) {\n  console.error('Session reset error:', e.message);\n}\n\nreturn [{\n  json: {\n    from: item.from,\n    message: message\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f6681ffc-734e-42fc-8a3c-e74bbbd49f8c",
      "name": "Send Final Result",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        1312,
        176
      ],
      "parameters": {
        "textBody": "={{ $json.message }}",
        "operation": "send",
        "phoneNumberId": "YOUR_PHONE_NUMBER_HERE",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $json.from }}"
      },
      "typeVersion": 1
    },
    {
      "id": "f7585811-4a59-4840-aa9d-af2e81cda42a",
      "name": "Upload Resume to Storage",
      "type": "n8n-nodes-base.code",
      "position": [
        64,
        336
      ],
      "parameters": {
        "jsCode": "// Upload PDF resume to Supabase Storage and save URL to DB\nconst SUPABASE_URL = 'https://zfyyiewkkjyzxnyiabvk.supabase.co';\nconst SUPABASE_KEY = 'eyJ_YOUR_JWT_TOKEN_HERE';\n\n// Get phone from Conversation State Manager\nconst phone = $('Conversation State Manager').item.json.from;\nconst safePhone = phone.replace(/[^a-zA-Z0-9]/g, '_');\nconst fileName = `${safePhone}_${Date.now()}.pdf`;\n\nlet resumeUrl = '$input.first().json.url';\n\ntry {\n  // \u2500\u2500 Find the binary field dynamically \u2500\u2500\n  // n8n may name the field 'data', 'file', or something else depending on the HTTP node config\n  const allBinary = items[0].binary || {};\n  const binaryKey = Object.keys(allBinary)[0]; // grab the first (and only) binary field\n\n  console.log('Available binary keys:', JSON.stringify(Object.keys(allBinary)));\n\n  if (!binaryKey) {\n    // No binary in items \u2014 try reading from $input\n    const inputItem = $input.first();\n    const inputBinary = inputItem.binary || {};\n    const inputKey = Object.keys(inputBinary)[0];\n    console.log('Input binary keys:', JSON.stringify(Object.keys(inputBinary)));\n\n    if (!inputKey) {\n      console.log('ERROR: No binary data found anywhere. Skipping upload.');\n      return [{ json: { resume_url: '', phone, error: 'no_binary' } }];\n    }\n  }\n\n  // Use n8n's native getBinaryDataBuffer \u2014 this is the CORRECT way to read binary in Code nodes\n  const buffer = await this.helpers.getBinaryDataBuffer(0, binaryKey || 'data');\n  console.log('Buffer size:', buffer.length, 'bytes');\n\n  if (!buffer || buffer.length === 0) {\n    console.log('ERROR: Buffer is empty. Skipping upload.');\n    return [{ json: { resume_url: '', phone, error: 'empty_buffer' } }];\n  }\n\n  // Upload raw PDF buffer to Supabase Storage\n  const uploadResponse = await this.helpers.httpRequest({\n    method: 'POST',\n    url: `${SUPABASE_URL}/storage/v1/object/resumes/${encodeURIComponent(fileName)}`,\n    headers: {\n      'apikey': SUPABASE_KEY,\n      'Authorization': `Bearer ${SUPABASE_KEY}`,\n      'Content-Type': 'application/pdf',\n      'x-upsert': 'true'\n    },\n    body: buffer\n  });\n\n  console.log('Upload response:', JSON.stringify(uploadResponse));\n\n  // Supabase Storage returns { Key: 'resumes/filename.pdf' } on success\n  if (uploadResponse && (uploadResponse.Key || uploadResponse.Id || uploadResponse.id)) {\n    resumeUrl = `${SUPABASE_URL}/storage/v1/object/public/resumes/${encodeURIComponent(fileName)}`;\n    console.log('Resume uploaded successfully:', resumeUrl);\n\n    // Save resume URL to whatsapp_sessions table\n    await this.helpers.httpRequest({\n      method: 'PATCH',\n      url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?phone=eq.${encodeURIComponent(phone)}`,\n      headers: {\n        'apikey': SUPABASE_KEY,\n        'Authorization': `Bearer ${SUPABASE_KEY}`,\n        'Content-Type': 'application/json',\n        'Prefer': 'return=minimal'\n      },\n      body: { resume_url: resumeUrl },\n      json: true\n    });\n    console.log('Resume URL saved to DB:', resumeUrl);\n  } else {\n    console.error('Upload may have failed. Response was:', JSON.stringify(uploadResponse));\n    // Still try to build the URL \u2014 Supabase sometimes returns 200 with empty body on upsert\n    resumeUrl = `${SUPABASE_URL}/storage/v1/object/public/resumes/${encodeURIComponent(fileName)}`;\n\n    await this.helpers.httpRequest({\n      method: 'PATCH',\n      url: `${SUPABASE_URL}/rest/v1/whatsapp_sessions?phone=eq.${encodeURIComponent(phone)}`,\n      headers: {\n        'apikey': SUPABASE_KEY,\n        'Authorization': `Bearer ${SUPABASE_KEY}`,\n        'Content-Type': 'application/json',\n        'Prefer': 'return=minimal'\n      },\n      body: { resume_url: resumeUrl },\n      json: true\n    });\n    console.log('Resume URL saved (optimistic):', resumeUrl);\n  }\n} catch(e) {\n  console.error('Upload error:', e.message, e.stack);\n}\n\nreturn [{ json: { resume_url: resumeUrl, phone } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e5a7b49f-21ef-483f-853f-f92c0b402ce5",
      "name": "Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1344,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 1276,
        "height": 456,
        "content": "## 1. Trigger & State\nHandles WhatsApp webhook and manages user conversation progress in Supabase."
      },
      "typeVersion": 1
    },
    {
      "id": "3b52f41f-325a-49aa-b26a-ad68bf7da034",
      "name": "Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 1516,
        "height": 456,
        "content": "## 2. Processing & Analysis\nDownloads resume, extracts text, performs scoring, and generates AI improvement suggestions."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Route Action": {
      "main": [
        [
          {
            "node": "Send Processing Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Conversation Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Resume URL": {
      "main": [
        [
          {
            "node": "Download Resume",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scoring Engine": {
      "main": [
        [
          {
            "node": "Merge Score + AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Resume": {
      "main": [
        [
          {
            "node": "Extract Resume Text",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upload Resume to Storage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Resume Data": {
      "main": [
        [
          {
            "node": "Scoring Engine",
            "type": "main",
            "index": 0
          },
          {
            "node": "OpenAI - Rejection Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Score + AI": {
      "main": [
        [
          {
            "node": "Format Result Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WhatsApp Trigger": {
      "main": [
        [
          {
            "node": "Conversation State Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Response": {
      "main": [
        [
          {
            "node": "Merge Score + AI",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Extract Resume Text": {
      "main": [
        [
          {
            "node": "Set Resume Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Result Message": {
      "main": [
        [
          {
            "node": "Send Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Processing Message": {
      "main": [
        [
          {
            "node": "Get Resume URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Conversation State Manager": {
      "main": [
        [
          {
            "node": "Route Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI - Rejection Analysis": {
      "main": [
        [
          {
            "node": "Parse AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}