0

Convert Lead

by
Published 8 days ago

Convert a lead into an account, contact, and (optionally) an opportunity via the SOAP convertLead call (Salesforce has no REST equivalent). converted_status must be a lead status marked as converted.

Script salesforce Verified

The script

Submitted by hugo989 Bun
Verified 9 days ago
1
//native
2

3
export type DynSelect_converted_status = string
4

5
// Dropdown of the org's "converted" lead statuses (LeadStatus where IsConverted = true).
6
export async function converted_status(auth: RT.Salesforce) {
7
  const apiVersion = auth.api_version || "v60.0"
8
  const url = new URL(`${auth.instance_url}/services/data/${apiVersion}/query`)
9
  url.searchParams.append(
10
    "q",
11
    "SELECT MasterLabel FROM LeadStatus WHERE IsConverted = true"
12
  )
13
  const response = await fetch(url, {
14
    headers: {
15
      Authorization: `Bearer ${auth.token}`,
16
      Accept: "application/json",
17
    },
18
  })
19
  if (!response.ok) {
20
    throw new Error(`${response.status} ${await response.text()}`)
21
  }
22
  const { records } = (await response.json()) as {
23
    records: { MasterLabel: string }[]
24
  }
25
  return records.map((r) => ({ value: r.MasterLabel, label: r.MasterLabel }))
26
}
27

28
function xmlEscape(s: string) {
29
  return s
30
    .replace(/&/g, "&")
31
    .replace(/</g, "&lt;")
32
    .replace(/>/g, "&gt;")
33
    .replace(/"/g, "&quot;")
34
    .replace(/'/g, "&apos;")
35
}
36

37
/**
38
 * Convert Lead
39
 * Convert a lead into an account, contact, and (optionally) an opportunity. Salesforce has no REST endpoint for this, so it uses the SOAP convertLead call with the same OAuth token. converted_status must be a lead status marked as converted.
40
 */
41
export async function main(
42
  auth: RT.Salesforce,
43
  lead_id: string,
44
  converted_status: DynSelect_converted_status,
45
  options:
46
    | {
47
        accountId?: string
48
        contactId?: string
49
        opportunityName?: string
50
        doNotCreateOpportunity?: boolean
51
        ownerId?: string
52
        overwriteLeadSource?: boolean
53
        sendNotificationEmail?: boolean
54
      }
55
    | undefined
56
) {
57
  const soapVersion = (auth.api_version || "v60.0").replace(/^v/, "")
58
  const o = options ?? {}
59
  const optional = [
60
    o.accountId !== undefined
61
      ? `<urn:accountId>${xmlEscape(o.accountId)}</urn:accountId>`
62
      : "",
63
    o.contactId !== undefined
64
      ? `<urn:contactId>${xmlEscape(o.contactId)}</urn:contactId>`
65
      : "",
66
    o.opportunityName !== undefined
67
      ? `<urn:opportunityName>${xmlEscape(o.opportunityName)}</urn:opportunityName>`
68
      : "",
69
    o.doNotCreateOpportunity !== undefined
70
      ? `<urn:doNotCreateOpportunity>${o.doNotCreateOpportunity}</urn:doNotCreateOpportunity>`
71
      : "",
72
    o.ownerId !== undefined
73
      ? `<urn:ownerId>${xmlEscape(o.ownerId)}</urn:ownerId>`
74
      : "",
75
    o.overwriteLeadSource !== undefined
76
      ? `<urn:overwriteLeadSource>${o.overwriteLeadSource}</urn:overwriteLeadSource>`
77
      : "",
78
    o.sendNotificationEmail !== undefined
79
      ? `<urn:sendNotificationEmail>${o.sendNotificationEmail}</urn:sendNotificationEmail>`
80
      : "",
81
  ].join("")
82

83
  const envelope = `<?xml version="1.0" encoding="UTF-8"?>
84
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:partner.soap.sforce.com">
85
  <soapenv:Header><urn:SessionHeader><urn:sessionId>${auth.token}</urn:sessionId></urn:SessionHeader></soapenv:Header>
86
  <soapenv:Body><urn:convertLead><urn:leadConverts>
87
    <urn:leadId>${xmlEscape(lead_id)}</urn:leadId>
88
    <urn:convertedStatus>${xmlEscape(converted_status)}</urn:convertedStatus>
89
    ${optional}
90
  </urn:leadConverts></urn:convertLead></soapenv:Body>
91
</soapenv:Envelope>`
92

93
  const response = await fetch(
94
    `${auth.instance_url}/services/Soap/u/${soapVersion}`,
95
    {
96
      method: "POST",
97
      headers: { "Content-Type": "text/xml; charset=UTF-8", SOAPAction: '""' },
98
      body: envelope,
99
    }
100
  )
101
  const text = await response.text()
102
  if (!response.ok) {
103
    throw new Error(`${response.status} ${text}`)
104
  }
105

106
  const pick = (tag: string) =>
107
    text.match(new RegExp(`<(?:\\w+:)?${tag}>(.*?)</(?:\\w+:)?${tag}>`))?.[1]
108
  if (pick("success") !== "true") {
109
    const message =
110
      text.match(/<faultstring>(.*?)<\/faultstring>/)?.[1] ??
111
      text.match(
112
        /<(?:\w+:)?errors>[\s\S]*?<(?:\w+:)?message>(.*?)<\/(?:\w+:)?message>/
113
      )?.[1] ??
114
      text
115
    throw new Error(`convertLead failed: ${message}`)
116
  }
117

118
  return {
119
    success: true,
120
    leadId: pick("leadId"),
121
    accountId: pick("accountId"),
122
    contactId: pick("contactId"),
123
    opportunityId: pick("opportunityId"),
124
  }
125
}
126