1 | |
2 |
|
3 | export type DynSelect_converted_status = string |
4 |
|
5 | |
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, "<") |
32 | .replace(/>/g, ">") |
33 | .replace(/"/g, """) |
34 | .replace(/'/g, "'") |
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 |
|