Last active
March 19, 2025 17:59
-
-
Save NickDeckerDevs/8c9f374ba589a9084cc0e76ebb7d7f33 to your computer and use it in GitHub Desktop.
I'm adding some pomp and circumstance to this initial workflow with a bunch of comments in here so that developers of all types can see some different things that are possible in workflows and maybe you learn something. You can also comment and talk about how MY code sucks! https://gist.githubusercontent.com/dennisedson/666742c45701e662075bc44f2…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// I console log all of this stuff in a way that I can easily understand where errors are | |
// coming from as well as the data this is being passed in. This is super helpful when you are | |
// trying to debug and figure out what is going wrong. I want into much more detail than I really | |
// wanted to here. I started this to not use the hubspot sdk, because I see no reason to use it | |
// if we already have axios installed. | |
// In my opinion it isn't worth using the hubspot sdk because there is a history of | |
// different versions of the sdk being available in serverless functions, workflows, and what | |
// is available for the newest version of the sdk. This can cause MAJOR ISSUES when you are | |
// trying to debug and figure out what is going wrong only to learn that this version of the sdk | |
// isn't supported in one of these environments. | |
// I'm also saying you shouldn't use the hubspot sdk, regardless of these issues, learning axios | |
// will remove your dependeny on using helper sdks in the future, and improve your skills as a | |
// developer not just in HubSpot, but across all your projects. | |
// this workflows uses object destructuring all over the place. If you are unfamiliar with this, | |
// please check out this resource: https://javascript.info/destructuring-assignment | |
// it is a great way to make your code more readable and easier to maintain. | |
const axios = require('axios'); | |
// replace this with your named secret in the custom coded action settings | |
// I like to place this at the top of the file because I will often create | |
// multiple functions to keep my main function clean, like in the example | |
// here. My secret is named hsApiKey, so that is what I use here! | |
const { hsApiKey } = process.env; | |
// this is where we set up the headers for the api call. We are using the named secret | |
// that we created in the custom coded action settings. no reason to rewrite this for | |
// every api call we need to make! | |
const hubSpotHeaders = { | |
'Authorization': `Bearer ${hsApiKey}`, | |
'Content-Type': 'application/json' | |
} | |
/* User Edtiable Variables */ | |
// this is where I will set up variables that a non techinical team member can go in | |
// and modify. In this case we are going to do the amount of days to add to the invoice date. | |
const daysToAddToInvoiceDate = 30; | |
// we are going to set up a variable here to store the error messages that happen | |
// in the try catch blocks we use in our for loop. This is useful for debugging the | |
// output data that is returned to the workflow. | |
const errorMessages = []; | |
// Helper function to add days to a date. | |
// I like to have all of my functions at the top of the file. | |
// this is useful for doing "const function" and you don't have to worry about | |
// hoisting the function. | |
function addDays(date, days) { | |
const result = new Date(date); | |
result.setDate(result.getDate() + days); | |
return result; | |
} | |
function logError(functionName, err) { | |
const error = err.response ? err.response.data : err.message; | |
console.log(`Error in ${functionName}(): ${error}`) | |
errorMessages.push(`${functionName}(): ${error}`) | |
} | |
// I like to create these seperate functions as this allows you to easily manage | |
// your library of api calls that do different things. I think this is important | |
// when you figure out what works best for you and your team, being able to share | |
// these or update them as needed for your own repositories. | |
// there is no need to stringify the properties here as because axios will do that for us | |
async function createInvoice(properties) { | |
console.log(`started createInvoice(${JSON.stringify(properties)})`); | |
try { | |
const invoiceResponse = await axios({ | |
method: 'post', | |
url: 'https://api.hubapi.com/crm/v3/objects/invoices', | |
headers: hubSpotHeaders, | |
data: { properties } | |
}) | |
const invoiceId = invoiceResponse.data.id | |
console.log(`Created invoice ${invoiceId}`) | |
return invoiceId | |
} catch (err) { | |
logError('createInvoice', err) | |
} | |
} | |
async function associateInvoiceWithDeal(invoiceId, dealId) { | |
console.log(`started associateInvoiceWithDeal(${invoiceId}, ${dealId})`); | |
try { | |
await axios({ | |
method: 'put', | |
url: `https://api.hubapi.com/crm/v4/objects/invoices/${invoiceId}/associations/default/deal/${dealId}`, | |
headers: hubSpotHeaders | |
}); | |
console.log(`Associated invoice ${invoiceId} with deal ${dealId}`); | |
} catch (err) { | |
logError('associateInvoiceWithDeal', err) | |
} | |
} | |
// Retrieve the contact associated with the deal and associate it with the invoice. | |
async function associateInvoiceWithContact(invoiceId, dealId) { | |
console.log(`started associateInvoiceWithContact(${invoiceId}, ${dealId})`); | |
// this is an example of destructing the response object that is returned.If we really only need | |
// the results from the response, we can avoid something like this. | |
// const dealResponse = await axios({... | |
// and because we know response.data is how this is set up we just do this | |
// const { data } = await axios({ | |
// method: 'get', | |
// url: `https://api.hubapi.com/crm/v3/objects/deals/${dealId}?associations=contact&archived=false`, | |
// headers: hubSpotHeaders | |
// }); | |
// but because we know the we are really looking for response.data.results we can just do this and make it | |
// super fancy and possibly unreadable by some people... so the choice is yours. | |
try { | |
const { data: { results } } = await axios({ | |
method: 'get', | |
url: `https://api.hubapi.com/crm/v3/objects/deals/${dealId}?associations=contact&archived=false`, | |
headers: hubSpotHeaders | |
}) | |
if (results.length > 0) { | |
const contactId = String(results[0].toObjectId); | |
await axios({ | |
method: 'put', | |
url: `https://api.hubapi.com/crm/v4/objects/invoices/${invoiceId}/associations/default/contact/${contactId}`, | |
headers: hubSpotHeaders | |
}); | |
console.log(`Associated invoice ${invoiceId} with contact ${contactId}`); | |
} else { | |
const message = `No contact found for deal ${dealId}` | |
console.log(message); | |
logError('associateInvoiceWithContact', message) | |
} | |
} catch (err) { | |
logError('associateInvoiceWithContact', err) | |
} | |
} | |
async function associateInvoiceWithCompany(invoiceId, dealId) { | |
console.log(`started associateInvoiceWithCompany(${invoiceId}, ${dealId})`); | |
try { | |
const { data: { results } } = await axios({ | |
method: 'get', | |
url: `https://api.hubapi.com/crm/v3/objects/deals/${dealId}?associations=company&archived=false`, | |
headers: hubSpotHeaders | |
}) | |
if (results.length > 0) { | |
const companyId = String(results[0].toObjectId); | |
await axios({ | |
method: 'put', | |
url: `https://api.hubapi.com/crm/v4/objects/invoices/${invoiceId}/associations/default/company/${companyId}`, | |
headers: hubSpotHeaders | |
}); | |
console.log(`Associated invoice ${invoiceId} with company ${companyId}`); | |
} else { | |
const message = `No company found for deal ${dealId}` | |
console.log(message); | |
logError('associateInvoiceWithCompany', message) | |
} | |
} catch (err) { | |
logError('associateInvoiceWithCompany', err) | |
} | |
} | |
async function getLineItemIds(dealId) { | |
console.log(`started getLineItemIds(${dealId})`); | |
// if you have more than 100 line items you will need to add pagination to this request | |
try { | |
const { data: { results } } = await axios({ | |
method: 'get', | |
url: `https://api.hubapi.com/crm/v3/objects/deals/${dealId}?associations=line_items&archived=false&limit=100`, | |
headers: hubSpotHeaders | |
}) | |
return results.map(({ toObjectId }) => String(toObjectId)); | |
} catch (err) { | |
logError('getLineItemIds', err) | |
} | |
} | |
async function updateInvoiceStatus(invoiceId, status) { | |
console.log(`started updateInvoiceStatus(${invoiceId}, ${status})`); | |
const properties = { | |
"hs_invoice_status": status | |
} | |
try { | |
await axios({ | |
method: 'patch', | |
url: `https://api.hubapi.com/crm/v3/objects/invoices/${invoiceId}`, | |
headers: hubSpotHeaders, | |
data: { properties } | |
}); | |
console.log(`Updated invoice ${invoiceId} status to ${status}`); | |
} catch (err) { | |
logError('updateInvoiceStatus', err) | |
} | |
} | |
exports.main = async (event, callback) => { | |
// Retrieve the deal record ID (hs_object_id) from the workflow input fields. | |
// I generally rename this in the workflow to dealId and will use | |
// const { dealId } = event.inputFields; | |
// this works as well, however in cases where you have multiple IDs coming in, this is complicated | |
// const { hs_object_id } = event.inputFields; | |
// screenshot of how I set this up: https://share.cleanshot.com/kNpKlK3C | |
const { dealId } = event.inputFields; | |
// Set invoice dates. | |
const invoiceDate = new Date(); | |
const dueDate = addDays(invoiceDate, daysToAddToInvoiceDate); | |
// STEP 1: Create a draft invoice. | |
const invoiceProperties = { | |
"hs_invoice_date": invoiceDate.toISOString(), | |
"hs_due_date": dueDate.toISOString(), | |
"hs_invoice_status": "draft", // Allowed values: "draft", "open", "paid", "voided" | |
"hs_currency": "USD" | |
}; | |
// Step 1: Create the invoice. | |
const invoiceId = await createInvoice(invoiceProperties); | |
// Step 2: Associate the invoice with the deal. | |
await associateInvoiceWithDeal(invoiceId, dealId); | |
// Step 3: Retrieve the contact associated with the deal and associate it with the invoice. | |
await associateInvoiceWithContact(invoiceId, dealId); | |
// STEP 3b: Retrieve the company associated with the deal and associate it with the invoice. | |
await associateInvoiceWithCompany(invoiceId, dealId); | |
// STEP 4: Retrieve the line items associated with the deal. | |
const dealLineItemIds = await getLineItemIds(dealId); | |
try { | |
// I'm just going to leave this alone here, we are using a for of loop | |
// because this is required when doing api calls that we need to await for | |
// a forEach or for loop will not work as they don't wait for the async call to complete | |
// I would likely place this in an async function but for the sake of how long I've spent on this | |
// I'm going to leave it as is. | |
// but normallyI would extract all these calls into other functions | |
// STEP 5: For each deal line item, create an equivalent line item and associate it with the invoice. | |
for (const lineItemId of dealLineItemIds) { | |
// Retrieve the original line item details. | |
// and because we have the other responses in functions, we can use our data in this and make it work | |
// and not have scope issues | |
// I'm doing just one try catch here because this is quite a bit of rework on this and I think I've | |
// made the examples as clear as possible and this is more of a learning example to help you | |
// do these workflows in a different way. | |
try { | |
const { data } = await axios({ | |
method: 'get', | |
url: `https://api.hubapi.com/crm/v3/objects/line_items/${lineItemId}`, | |
headers: hubSpotHeaders | |
}); | |
// Extract details—adjust property keys as needed. | |
const { name, quantity, price, hs_product_id, hs_tax_rate_group_id } = data.properties; | |
// because we are doing destructing here, it allows us to use variables like properties | |
// and we don't have to worry about naming them newLineItemProperties or anything like that | |
// Build new line item properties. | |
let properties = { | |
"name": name, | |
"quantity": quantity, | |
"price": price | |
}; | |
if (hs_product_id) { | |
properties.hs_product_id = hs_product_id; | |
} | |
if (hs_tax_rate_group_id) { | |
properties.hs_tax_rate_group_id = hs_tax_rate_group_id; | |
} | |
// to show a differnet way to destructure the response object isntead of doing our above const { data } = await axios({ | |
// we are going to do this a bit different! | |
// where we destructure the response object and pull out the id, maing it newLineItemId | |
// Create the new line item. | |
const { data: { id: newLineItemId } } = await axios({ | |
method: 'post', | |
url: 'https://api.hubapi.com/crm/v3/objects/line_items', | |
headers: hubSpotHeaders, | |
data: { properties } | |
}); | |
console.log(`Created new line item ${newLineItemId} based on deal line item ${lineItemId}`); | |
// Associate the new line item with the invoice using the default association endpoint. | |
await axios({ | |
method: 'put', | |
url: `https://api.hubapi.com/crm/v4/objects/invoices/${invoiceId}/associations/default/line_item/${newLineItemId}`, | |
headers: hubSpotHeaders | |
}); | |
console.log(`Associated new line item ${newLineItemId} with invoice ${invoiceId}`); | |
} catch (err) { | |
logError(`main() lineItemId: ${lineItemId}`, err) | |
} | |
} | |
// STEP 6: Update the invoice status to "open". | |
await updateInvoiceStatus(invoiceId, 'open'); | |
callback({ | |
outputFields: { | |
invoiceId: invoiceId, | |
errorMessages: errorMessages | |
} | |
}); | |
} catch (err) { | |
logError('main()', err) | |
// add this to your output fields -- this really helps out when you are debugging this at a larger scale and later date | |
callback({ | |
outputFields: { | |
errorMessages: errorMessages | |
} | |
}); | |
} | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment