{
  "name": "REVOPS-LI-005 | 04 | Approved HubSpot merge executor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 2
            }
          ]
        }
      },
      "id": "51b9115f-b8c0-472d-8f95-5057c569629a",
      "name": "Check Approved Queue",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {},
      "id": "39f6228d-8219-44a1-b068-cc8e39ff471d",
      "name": "Run Executor Manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        270,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:{config:{execute_merges:false,api_version:'v3',stale_merge_minutes:5}}}];"
      },
      "id": "8b5a015f-a73b-4d3e-bc36-a0bd4e271c6f",
      "name": "Merge Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        540,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "afa8fd32-64b6-49b6-a299-bd03d365306a",
              "leftValue": "={{ $json.config.execute_merges === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "640da95e-67f5-4623-9e4f-cfc636d1bb2f",
      "name": "Execution Enabled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        810,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.claim_merge($1::text,$2::integer) AS candidate;",
        "options": {
          "queryReplacement": "={{ [String($execution.id),$json.config.stale_merge_minutes] }}"
        }
      },
      "id": "5698a80d-ef7f-4c98-aa97-664592153597",
      "name": "Claim Approved Merge",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1080,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "72925444-2d9a-4c48-8881-d762ce077d99",
              "leftValue": "={{ $json.candidate !== null }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "8be48282-0f03-433c-b8d0-3001b07d53a3",
      "name": "Candidate Found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1350,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const D = (function duplicateLibrary(){\n  const text=v=>String(v??'').trim();\n  const lower=v=>text(v).toLowerCase();\n  const digits=v=>text(v).replace(/\\D/g,'');\n  const host=v=>{try{return new URL(/^https?:\\/\\//i.test(text(v))?text(v):'https://'+text(v)).hostname.toLowerCase().replace(/^www\\./,'')}catch{return lower(v).replace(/^www\\./,'').split('/')[0]}};\n  const normName=v=>lower(v).normalize('NFKD').replace(/[\\u0300-\\u036f]/g,'').replace(/[^a-z0-9]+/g,' ').trim();\n  const tokens=v=>new Set(normName(v).split(' ').filter(x=>x.length>1));\n  const similarity=(a,b)=>{const A=tokens(a),B=tokens(b);if(!A.size||!B.size)return 0;let hit=0;for(const x of A)if(B.has(x))hit++;return hit/Math.max(A.size,B.size)};\n  const truthy=v=>['true','1','yes','on'].includes(lower(v));\n  const prop=(r,k)=>r?.properties?.[k]??'';\n  function score(source,candidate,type){\n    const s=[],p=source.properties||{},q=candidate.properties||{};let score=0;\n    const add=(points,signal)=>{score=Math.max(score,points);s.push({signal,points})};\n    if(type==='contacts'){\n      if(lower(p.email)&&lower(p.email)===lower(q.email))add(100,'EXACT_EMAIL');\n      if(lower(p.revops_linkedin_profile_url)&&lower(p.revops_linkedin_profile_url)===lower(q.revops_linkedin_profile_url))add(95,'EXACT_LINKEDIN');\n      const sameName=normName(p.firstname+' '+p.lastname)===normName(q.firstname+' '+q.lastname)&&normName(p.firstname+' '+p.lastname);\n      const samePhone=digits(p.phone||p.mobilephone).length>=7&&digits(p.phone||p.mobilephone)===digits(q.phone||q.mobilephone);\n      if(samePhone&&sameName)add(90,'PHONE_AND_NAME');\n      const sd=host(p.website),qd=host(q.website);if(sameName&&sd&&sd===qd)add(85,'NAME_AND_COMPANY_DOMAIN');\n      if(sd&&sd===qd&&similarity(p.firstname+' '+p.lastname,q.firstname+' '+q.lastname)>=.6)add(80,'DOMAIN_AND_SIMILAR_NAME');\n      if(similarity(p.firstname+' '+p.lastname,q.firstname+' '+q.lastname)>=.8&&similarity(p.company,q.company)>=.7)add(60,'SIMILAR_PERSON_AND_COMPANY');\n      const su=lower(p.email).split('@')[0],qu=lower(q.email).split('@')[0];if(su&&su===qu)add(40,'EMAIL_USERNAME_ONLY');\n      if(lower(p.firstname)&&lower(p.firstname)===lower(q.firstname))add(10,'FIRST_NAME_ONLY');\n    }else{\n      if(lower(p.revops_company_linkedin_url)&&lower(p.revops_company_linkedin_url)===lower(q.revops_company_linkedin_url))add(80,'EXACT_COMPANY_LINKEDIN');\n      const sd=host(p.domain||p.website),qd=host(q.domain||q.website);if(sd&&sd===qd)add(75,'EXACT_COMPANY_DOMAIN');\n      if(similarity(p.name,q.name)>=.8&&lower(p.country)&&lower(p.country)===lower(q.country))add(65,'SIMILAR_COMPANY_AND_COUNTRY');\n    }\n    return {score,signals:s.sort((a,b)=>b.points-a.points)};\n  }\n  const completeness=(r,keys)=>keys.reduce((n,k)=>n+(text(prop(r,k))?1:0),0);\n  function rank(r,type){const p=r.properties||{};const life={customer:50,opportunity:40,salesqualifiedlead:30,marketingqualifiedlead:20,lead:10};return (life[lower(p.lifecyclestage)]||0)+(Number(p.num_associated_deals||0)>0?30:0)+(text(p.hubspot_owner_id)?10:0)+completeness(r,type==='contacts'?['email','phone','mobilephone','jobtitle','firstname','lastname','company','website']:['name','domain','phone','country','website'])*2;}\n  function choosePrimary(a,b,type){const ar=rank(a,type),br=rank(b,type);if(ar!==br)return ar>br?a:b;const ad=Date.parse(a.createdAt||prop(a,'createdate')||'9999-12-31'),bd=Date.parse(b.createdAt||prop(b,'createdate')||'9999-12-31');return ad<=bd?a:b;}\n  function classify(n){return n>=90?'DUPLICATE_CONFIRMED':n>=60?'DUPLICATE_REVIEW_REQUIRED':n>=40?'POSSIBLE_DUPLICATE':n>0?'DUPLICATE_WEAK_MATCH':'NO_DUPLICATE_FOUND'}\n  function buildSearches(source,type){const p=source.properties||{},out=[];const exact=(propertyName,value)=>{if(text(value))out.push({kind:'exact',propertyName,value:text(value),body:{filterGroups:[{filters:[{propertyName,operator:'EQ',value:text(value)}]}],properties:type==='contacts'?CONTACT_PROPS:COMPANY_PROPS,limit:100}})};\n    if(type==='contacts'){exact('email',lower(p.email));exact('phone',p.phone);exact('mobilephone',p.mobilephone);if(text(p.firstname+' '+p.lastname))out.push({kind:'fuzzy',propertyName:'name',value:text(p.firstname+' '+p.lastname),body:{query:text(p.firstname+' '+p.lastname),properties:CONTACT_PROPS,limit:100}})}\n    else{exact('domain',host(p.domain));exact('website',p.website);if(text(p.name))out.push({kind:'fuzzy',propertyName:'name',value:text(p.name),body:{query:text(p.name),properties:COMPANY_PROPS,limit:100}})}\n    return out;\n  }\n  function best(source,responses,type){const map=new Map();for(const response of responses){const body=response.body||response;for(const r of body.results||[])if(String(r.id)!==String(source.id))map.set(String(r.id),r)}let winner=null;for(const r of map.values()){const scored=score(source,r,type);if(!winner||scored.score>winner.score)winner={record:r,...scored}}if(!winner)return {found:false,status:'NO_DUPLICATE_FOUND'};const primary=choosePrimary(source,winner.record,type),secondary=String(primary.id)===String(source.id)?winner.record:source;return {found:winner.score>=40,status:classify(winner.score),score:winner.score,signals:winner.signals,primary,secondary,source_snapshot:source,candidate_snapshot:winner.record};}\n  function mergePlan(candidate,records,config){const bySide=Object.fromEntries(records.map(x=>[x.side,x.record]));const primary=bySide.primary,secondary=bySide.secondary,blockers=[];if(!primary||!secondary)blockers.push('RECORD_FETCH_FAILED');if(!primary||!secondary)return {safe:false,blockers};\n    if(String(primary.id)===String(secondary.id))blockers.push('RECORDS_ALREADY_RESOLVE_TO_SAME_ID');\n    const expected=candidate.snapshots||{};for(const [side,r] of Object.entries({primary,secondary})){const old=[expected.primary,expected.secondary].find(x=>String(x?.id||'')===String(r.id));if(old?.updatedAt&&r.updatedAt&&old.updatedAt!==r.updatedAt)blockers.push(side.toUpperCase()+'_CHANGED_AFTER_REVIEW');}\n    const pp=primary.properties||{},sp=secondary.properties||{};if(text(pp.hubspot_owner_id)&&text(sp.hubspot_owner_id)&&pp.hubspot_owner_id!==sp.hubspot_owner_id&&!candidate.manager_override)blockers.push('DIFFERENT_OWNERS_REQUIRE_MANAGER_OVERRIDE');\n    if(candidate.object_type==='contacts'){\n      if(truthy(pp.revops_do_not_contact)||truthy(sp.revops_do_not_contact))blockers.push('DO_NOT_CONTACT_PRESENT');\n      if(text(pp.hs_email_optout)!==text(sp.hs_email_optout)&&text(pp.hs_email_optout)&&text(sp.hs_email_optout))blockers.push('CONSENT_CONFLICT');\n      const companies=r=>(r.associations?.companies?.results||[]).map(x=>String(x.id)).sort();const a=companies(primary),b=companies(secondary);if(a.length&&b.length&&JSON.stringify(a)!==JSON.stringify(b))blockers.push('DIFFERENT_COMPANY_ASSOCIATIONS');\n      if(truthy(pp.revops_sequence_enrolled)||truthy(sp.revops_sequence_enrolled))blockers.push('ACTIVE_SEQUENCE_REVIEW_REQUIRED');\n    }\n    const deals=r=>(r.associations?.deals?.results||[]).map(x=>String(x.id)).sort();const da=deals(primary),db=deals(secondary);if(da.length&&db.length&&JSON.stringify(da)!==JSON.stringify(db))blockers.push('DIFFERENT_DEAL_ASSOCIATIONS');\n    const safeKeys=candidate.object_type==='contacts'?['phone','mobilephone','jobtitle','revops_linkedin_profile_url','website','company']:['phone','website','country','city','state','zip','revops_company_linkedin_url'];const patch={};for(const k of safeKeys)if(!text(pp[k])&&text(sp[k]))patch[k]=sp[k];\n    return {safe:blockers.length===0,blockers,patch,primary,secondary,merge_body:{primaryObjectId:String(primary.id),objectIdToMerge:String(secondary.id)},execute:config.execute_merges===true};\n  }\n  const CONTACT_PROPS=['email','firstname','lastname','phone','mobilephone','jobtitle','company','website','lifecyclestage','hubspot_owner_id','num_associated_deals','hs_email_optout','hs_email_bad_address','revops_do_not_contact','revops_sequence_enrolled','revops_linkedin_profile_url','revops_duplicate_status','createdate','lastmodifieddate'];\n  const COMPANY_PROPS=['name','domain','website','phone','country','city','state','zip','lifecyclestage','hubspot_owner_id','num_associated_deals','revops_company_linkedin_url','revops_duplicate_status','createdate','lastmodifieddate'];\n  return {text,lower,digits,host,normName,similarity,score,choosePrimary,classify,buildSearches,best,mergePlan,CONTACT_PROPS,COMPANY_PROPS};\n})();\nconst c=$('Claim Approved Merge').first().json.candidate;const properties=c.object_type==='contacts'?D.CONTACT_PROPS:D.COMPANY_PROPS;return [{json:{side:'primary',id:c.approved_primary_id,candidate:c,properties}},{json:{side:'secondary',id:c.approved_secondary_id,candidate:c,properties}}];"
      },
      "id": "fa8f3710-1a18-4550-8a0a-fd048d8d3e65",
      "name": "Build Fresh Record Reads",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        0
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.hubapi.com/crm/v3/objects/'+$json.candidate.object_type+'/'+$json.id+'?properties='+encodeURIComponent($json.properties.join(','))+'&associations=companies,deals' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "9e40cef5-8ed1-4fa7-b301-fc31be6d3aae",
      "name": "Read Fresh Records",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1890,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const D = (function duplicateLibrary(){\n  const text=v=>String(v??'').trim();\n  const lower=v=>text(v).toLowerCase();\n  const digits=v=>text(v).replace(/\\D/g,'');\n  const host=v=>{try{return new URL(/^https?:\\/\\//i.test(text(v))?text(v):'https://'+text(v)).hostname.toLowerCase().replace(/^www\\./,'')}catch{return lower(v).replace(/^www\\./,'').split('/')[0]}};\n  const normName=v=>lower(v).normalize('NFKD').replace(/[\\u0300-\\u036f]/g,'').replace(/[^a-z0-9]+/g,' ').trim();\n  const tokens=v=>new Set(normName(v).split(' ').filter(x=>x.length>1));\n  const similarity=(a,b)=>{const A=tokens(a),B=tokens(b);if(!A.size||!B.size)return 0;let hit=0;for(const x of A)if(B.has(x))hit++;return hit/Math.max(A.size,B.size)};\n  const truthy=v=>['true','1','yes','on'].includes(lower(v));\n  const prop=(r,k)=>r?.properties?.[k]??'';\n  function score(source,candidate,type){\n    const s=[],p=source.properties||{},q=candidate.properties||{};let score=0;\n    const add=(points,signal)=>{score=Math.max(score,points);s.push({signal,points})};\n    if(type==='contacts'){\n      if(lower(p.email)&&lower(p.email)===lower(q.email))add(100,'EXACT_EMAIL');\n      if(lower(p.revops_linkedin_profile_url)&&lower(p.revops_linkedin_profile_url)===lower(q.revops_linkedin_profile_url))add(95,'EXACT_LINKEDIN');\n      const sameName=normName(p.firstname+' '+p.lastname)===normName(q.firstname+' '+q.lastname)&&normName(p.firstname+' '+p.lastname);\n      const samePhone=digits(p.phone||p.mobilephone).length>=7&&digits(p.phone||p.mobilephone)===digits(q.phone||q.mobilephone);\n      if(samePhone&&sameName)add(90,'PHONE_AND_NAME');\n      const sd=host(p.website),qd=host(q.website);if(sameName&&sd&&sd===qd)add(85,'NAME_AND_COMPANY_DOMAIN');\n      if(sd&&sd===qd&&similarity(p.firstname+' '+p.lastname,q.firstname+' '+q.lastname)>=.6)add(80,'DOMAIN_AND_SIMILAR_NAME');\n      if(similarity(p.firstname+' '+p.lastname,q.firstname+' '+q.lastname)>=.8&&similarity(p.company,q.company)>=.7)add(60,'SIMILAR_PERSON_AND_COMPANY');\n      const su=lower(p.email).split('@')[0],qu=lower(q.email).split('@')[0];if(su&&su===qu)add(40,'EMAIL_USERNAME_ONLY');\n      if(lower(p.firstname)&&lower(p.firstname)===lower(q.firstname))add(10,'FIRST_NAME_ONLY');\n    }else{\n      if(lower(p.revops_company_linkedin_url)&&lower(p.revops_company_linkedin_url)===lower(q.revops_company_linkedin_url))add(80,'EXACT_COMPANY_LINKEDIN');\n      const sd=host(p.domain||p.website),qd=host(q.domain||q.website);if(sd&&sd===qd)add(75,'EXACT_COMPANY_DOMAIN');\n      if(similarity(p.name,q.name)>=.8&&lower(p.country)&&lower(p.country)===lower(q.country))add(65,'SIMILAR_COMPANY_AND_COUNTRY');\n    }\n    return {score,signals:s.sort((a,b)=>b.points-a.points)};\n  }\n  const completeness=(r,keys)=>keys.reduce((n,k)=>n+(text(prop(r,k))?1:0),0);\n  function rank(r,type){const p=r.properties||{};const life={customer:50,opportunity:40,salesqualifiedlead:30,marketingqualifiedlead:20,lead:10};return (life[lower(p.lifecyclestage)]||0)+(Number(p.num_associated_deals||0)>0?30:0)+(text(p.hubspot_owner_id)?10:0)+completeness(r,type==='contacts'?['email','phone','mobilephone','jobtitle','firstname','lastname','company','website']:['name','domain','phone','country','website'])*2;}\n  function choosePrimary(a,b,type){const ar=rank(a,type),br=rank(b,type);if(ar!==br)return ar>br?a:b;const ad=Date.parse(a.createdAt||prop(a,'createdate')||'9999-12-31'),bd=Date.parse(b.createdAt||prop(b,'createdate')||'9999-12-31');return ad<=bd?a:b;}\n  function classify(n){return n>=90?'DUPLICATE_CONFIRMED':n>=60?'DUPLICATE_REVIEW_REQUIRED':n>=40?'POSSIBLE_DUPLICATE':n>0?'DUPLICATE_WEAK_MATCH':'NO_DUPLICATE_FOUND'}\n  function buildSearches(source,type){const p=source.properties||{},out=[];const exact=(propertyName,value)=>{if(text(value))out.push({kind:'exact',propertyName,value:text(value),body:{filterGroups:[{filters:[{propertyName,operator:'EQ',value:text(value)}]}],properties:type==='contacts'?CONTACT_PROPS:COMPANY_PROPS,limit:100}})};\n    if(type==='contacts'){exact('email',lower(p.email));exact('phone',p.phone);exact('mobilephone',p.mobilephone);if(text(p.firstname+' '+p.lastname))out.push({kind:'fuzzy',propertyName:'name',value:text(p.firstname+' '+p.lastname),body:{query:text(p.firstname+' '+p.lastname),properties:CONTACT_PROPS,limit:100}})}\n    else{exact('domain',host(p.domain));exact('website',p.website);if(text(p.name))out.push({kind:'fuzzy',propertyName:'name',value:text(p.name),body:{query:text(p.name),properties:COMPANY_PROPS,limit:100}})}\n    return out;\n  }\n  function best(source,responses,type){const map=new Map();for(const response of responses){const body=response.body||response;for(const r of body.results||[])if(String(r.id)!==String(source.id))map.set(String(r.id),r)}let winner=null;for(const r of map.values()){const scored=score(source,r,type);if(!winner||scored.score>winner.score)winner={record:r,...scored}}if(!winner)return {found:false,status:'NO_DUPLICATE_FOUND'};const primary=choosePrimary(source,winner.record,type),secondary=String(primary.id)===String(source.id)?winner.record:source;return {found:winner.score>=40,status:classify(winner.score),score:winner.score,signals:winner.signals,primary,secondary,source_snapshot:source,candidate_snapshot:winner.record};}\n  function mergePlan(candidate,records,config){const bySide=Object.fromEntries(records.map(x=>[x.side,x.record]));const primary=bySide.primary,secondary=bySide.secondary,blockers=[];if(!primary||!secondary)blockers.push('RECORD_FETCH_FAILED');if(!primary||!secondary)return {safe:false,blockers};\n    if(String(primary.id)===String(secondary.id))blockers.push('RECORDS_ALREADY_RESOLVE_TO_SAME_ID');\n    const expected=candidate.snapshots||{};for(const [side,r] of Object.entries({primary,secondary})){const old=[expected.primary,expected.secondary].find(x=>String(x?.id||'')===String(r.id));if(old?.updatedAt&&r.updatedAt&&old.updatedAt!==r.updatedAt)blockers.push(side.toUpperCase()+'_CHANGED_AFTER_REVIEW');}\n    const pp=primary.properties||{},sp=secondary.properties||{};if(text(pp.hubspot_owner_id)&&text(sp.hubspot_owner_id)&&pp.hubspot_owner_id!==sp.hubspot_owner_id&&!candidate.manager_override)blockers.push('DIFFERENT_OWNERS_REQUIRE_MANAGER_OVERRIDE');\n    if(candidate.object_type==='contacts'){\n      if(truthy(pp.revops_do_not_contact)||truthy(sp.revops_do_not_contact))blockers.push('DO_NOT_CONTACT_PRESENT');\n      if(text(pp.hs_email_optout)!==text(sp.hs_email_optout)&&text(pp.hs_email_optout)&&text(sp.hs_email_optout))blockers.push('CONSENT_CONFLICT');\n      const companies=r=>(r.associations?.companies?.results||[]).map(x=>String(x.id)).sort();const a=companies(primary),b=companies(secondary);if(a.length&&b.length&&JSON.stringify(a)!==JSON.stringify(b))blockers.push('DIFFERENT_COMPANY_ASSOCIATIONS');\n      if(truthy(pp.revops_sequence_enrolled)||truthy(sp.revops_sequence_enrolled))blockers.push('ACTIVE_SEQUENCE_REVIEW_REQUIRED');\n    }\n    const deals=r=>(r.associations?.deals?.results||[]).map(x=>String(x.id)).sort();const da=deals(primary),db=deals(secondary);if(da.length&&db.length&&JSON.stringify(da)!==JSON.stringify(db))blockers.push('DIFFERENT_DEAL_ASSOCIATIONS');\n    const safeKeys=candidate.object_type==='contacts'?['phone','mobilephone','jobtitle','revops_linkedin_profile_url','website','company']:['phone','website','country','city','state','zip','revops_company_linkedin_url'];const patch={};for(const k of safeKeys)if(!text(pp[k])&&text(sp[k]))patch[k]=sp[k];\n    return {safe:blockers.length===0,blockers,patch,primary,secondary,merge_body:{primaryObjectId:String(primary.id),objectIdToMerge:String(secondary.id)},execute:config.execute_merges===true};\n  }\n  const CONTACT_PROPS=['email','firstname','lastname','phone','mobilephone','jobtitle','company','website','lifecyclestage','hubspot_owner_id','num_associated_deals','hs_email_optout','hs_email_bad_address','revops_do_not_contact','revops_sequence_enrolled','revops_linkedin_profile_url','revops_duplicate_status','createdate','lastmodifieddate'];\n  const COMPANY_PROPS=['name','domain','website','phone','country','city','state','zip','lifecyclestage','hubspot_owner_id','num_associated_deals','revops_company_linkedin_url','revops_duplicate_status','createdate','lastmodifieddate'];\n  return {text,lower,digits,host,normName,similarity,score,choosePrimary,classify,buildSearches,best,mergePlan,CONTACT_PROPS,COMPANY_PROPS};\n})();\nconst request=$('Build Fresh Record Reads').all();const responses=$input.all();const records=responses.map((x,i)=>({side:request[i].json.side,record:x.json.statusCode===200?x.json.body:null,statusCode:x.json.statusCode}));const candidate=request[0].json.candidate;if(candidate.reconcile_only){const ids=records.map(x=>String(x.record?.id||''));return [{json:{candidate,reconcile_only:true,reconciled:ids[0]&&ids[0]===ids[1],result_id:ids[0]||null,records}}]}const plan=D.mergePlan(candidate,records,$('Merge Configuration').first().json.config);return [{json:{candidate,records,...plan}}];"
      },
      "id": "59ca7ace-574a-418a-9557-3479301be8d8",
      "name": "Enforce Merge Blockers",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.audit_premerge($1::uuid,$2::jsonb,$3::jsonb) AS saved;",
        "options": {
          "queryReplacement": "={{ [$json.candidate.candidate_id,JSON.stringify($json.records),JSON.stringify({blockers:$json.blockers||[],patch:$json.patch||{},reconcile_only:$json.reconcile_only||false})] }}"
        }
      },
      "id": "0788e6fb-d014-4058-a56e-a7161ddfc31a",
      "name": "Save Immutable Pre-Merge Snapshot",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        270,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1a804c7-48dc-4562-9cb2-376c2a35408c",
              "leftValue": "={{ $('Enforce Merge Blockers').first().json.reconcile_only === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "15ff6258-fcf0-4d19-b689-4fae5e2570b8",
      "name": "Reconciliation Run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        540,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "2596487a-750b-4787-8520-bd2a66da4a63",
              "leftValue": "={{ $('Enforce Merge Blockers').first().json.reconciled === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cfcd791a-0f41-46d0-953b-9f32860f167d",
      "name": "Merge Already Completed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        810,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.complete_merge($1::uuid,$2::text,$3::jsonb);",
        "options": {
          "queryReplacement": "={{ [$('Enforce Merge Blockers').first().json.candidate.candidate_id,$('Enforce Merge Blockers').first().json.result_id,JSON.stringify({method:'old_ids_resolve_same'})] }}"
        }
      },
      "id": "5feb7655-6068-4e53-927b-e1286527a14d",
      "name": "Complete Reconciled Merge",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1080,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.block_merge($1::uuid,$2::jsonb);",
        "options": {
          "queryReplacement": "={{ [$('Enforce Merge Blockers').first().json.candidate.candidate_id,JSON.stringify(['AMBIGUOUS_PRIOR_ATTEMPT_REQUIRES_REVIEW'])] }}"
        }
      },
      "id": "00717cce-2123-4d58-b11e-9bdad5f6f37d",
      "name": "Stop Ambiguous Retry",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1350,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "601c44a4-27f8-445c-9fa1-839b05fe926f",
              "leftValue": "={{ $('Enforce Merge Blockers').first().json.safe === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "30741c8b-9852-4e85-af55-b854cc4d7d2f",
      "name": "Safe To Merge?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1620,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.block_merge($1::uuid,$2::jsonb);",
        "options": {
          "queryReplacement": "={{ [$('Enforce Merge Blockers').first().json.candidate.candidate_id,JSON.stringify($('Enforce Merge Blockers').first().json.blockers)] }}"
        }
      },
      "id": "67b43da0-468a-4539-9ace-fb0c6499e07c",
      "name": "Record Blockers",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1890,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "084e1294-6856-4d6a-aea8-42c0e5853d36",
              "leftValue": "={{ Object.keys($('Enforce Merge Blockers').first().json.patch || {}).length > 0 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3d9e27d5-dc48-4bfc-bde5-9d7d8e5bc7fd",
      "name": "Safe Patch Needed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        0,
        600
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.hubapi.com/crm/v3/objects/'+$('Enforce Merge Blockers').first().json.candidate.object_type+'/'+$('Enforce Merge Blockers').first().json.candidate.approved_primary_id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ {properties:$('Enforce Merge Blockers').first().json.patch} }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "9989d9df-17e3-493c-9b58-6c118877f09e",
      "name": "Fill Empty Safe Fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        270,
        600
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ 'https://api.hubapi.com/crm/v3/objects/'+$('Enforce Merge Blockers').first().json.candidate.object_type+'/merge' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ $('Enforce Merge Blockers').first().json.merge_body }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "020f653c-43d5-4854-b4dd-710fcfab3975",
      "name": "Execute Native HubSpot Merge",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        540,
        600
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const r=$input.first().json;if(r.statusCode!==200||!r.body?.id)throw new Error('Merge response was not a confirmed 200 with result ID');return [{json:{candidate:$('Enforce Merge Blockers').first().json.candidate,result_id:String(r.body.id),response:r.body}}];"
      },
      "id": "22172772-8c0b-42ea-b611-1a69a203a638",
      "name": "Validate Merge Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        810,
        600
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT duplicate_cleanup.complete_merge($1::uuid,$2::text,$3::jsonb);",
        "options": {
          "queryReplacement": "={{ [$json.candidate.candidate_id,$json.result_id,JSON.stringify($json.response)] }}"
        }
      },
      "id": "d335a5aa-ae28-4e21-a111-656ac2d9a89c",
      "name": "Complete Merge Audit",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1080,
        600
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "content": "## Destructive executor — disabled by default\nSet execute_merges=true only after sandbox/UAT. Every candidate still needs one-time approval and fresh blocker checks.\nThe native merge node has no automatic retry. A stale MERGING item is reconciled by reading both old IDs; ambiguity is sent back to review.\nHubSpot creates a new result ID. Both old IDs resolve to it, and the result ID is stored in the audit vault.",
        "width": 700,
        "height": 220
      },
      "id": "a647d2ad-5f53-40ba-a37f-3e8a8e91915a",
      "name": "Workflow Guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -270
      ]
    }
  ],
  "connections": {
    "Check Approved Queue": {
      "main": [
        [
          {
            "node": "Merge Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Executor Manually": {
      "main": [
        [
          {
            "node": "Merge Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Configuration": {
      "main": [
        [
          {
            "node": "Execution Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execution Enabled?": {
      "main": [
        [
          {
            "node": "Claim Approved Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim Approved Merge": {
      "main": [
        [
          {
            "node": "Candidate Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Candidate Found?": {
      "main": [
        [
          {
            "node": "Build Fresh Record Reads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Fresh Record Reads": {
      "main": [
        [
          {
            "node": "Read Fresh Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Fresh Records": {
      "main": [
        [
          {
            "node": "Enforce Merge Blockers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enforce Merge Blockers": {
      "main": [
        [
          {
            "node": "Save Immutable Pre-Merge Snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Immutable Pre-Merge Snapshot": {
      "main": [
        [
          {
            "node": "Reconciliation Run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reconciliation Run?": {
      "main": [
        [
          {
            "node": "Merge Already Completed?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe To Merge?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Already Completed?": {
      "main": [
        [
          {
            "node": "Complete Reconciled Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop Ambiguous Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Safe To Merge?": {
      "main": [
        [
          {
            "node": "Safe Patch Needed?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Record Blockers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Safe Patch Needed?": {
      "main": [
        [
          {
            "node": "Fill Empty Safe Fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Execute Native HubSpot Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill Empty Safe Fields": {
      "main": [
        [
          {
            "node": "Execute Native HubSpot Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Native HubSpot Merge": {
      "main": [
        [
          {
            "node": "Validate Merge Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Merge Response": {
      "main": [
        [
          {
            "node": "Complete Merge Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Tbilisi",
    "saveExecutionProgress": true
  },
  "active": false,
  "pinData": {},
  "tags": []
}
