Custom Code Action event object structure dealstage

Hi,

I am trying to figure out how to work with dealstage from the event object in custom code actions. I am receiving the following logs:

2023-11-01T15:40:53.378Z	INFO	{"timestamp":"2023-11-01T15:40:53.378Z","message":"Event received","data":{"event":{"callbackId":"a7d31b0f-945a-42d0-9fa5-e9e0aa8e08ad","origin":{"portalId":20745819,"actionDefinitionId":48171846,"actionDefinitionVersion":0,"actionExecutionIndexIdentifier":null,"extensionDefinitionId":48171846,"extensionDefinitionVersionId":0},"object":{"objectId":15796334605,"objectType":"DEAL"},"fields":{"hs_object_id":"15796334605","DEALSTAGE":"115192911"},"inputFields":{"hs_object_id":"15796334605","DEALSTAGE":"115192911"}}}}
2023-11-01T15:40:53.380Z	INFO	{"timestamp":"2023-11-01T15:40:53.380Z","message":"Deal object","data":{"deal":{"objectId":15796334605,"objectType":"DEAL"}}}
2023-11-01T15:40:53.380Z	INFO	{"timestamp":"2023-11-01T15:40:53.380Z","message":"INFO","data":"Deal object"}
2023-11-01T15:40:53.402Z	ERROR	Unhandled Promise Rejection 	{"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"TypeError: Cannot read properties of undefined (reading 'DEALSTAGE')","reason":{"errorType":"TypeError","errorMessage":"Cannot read properties of undefined (reading 'DEALSTAGE')","stack":["TypeError: Cannot read properties of undefined (reading 'DEALSTAGE')"," at initiateAndProcessDeal (/var/task/file.js:160:43)"," at Object.exports.main (/var/task/file.js:300:9)"," at Runtime.exports.hubspot_handler [as handler] (/var/task/hubspotHandler.js:6:21)"," at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)"]},"promise":{},"stack":["Runtime.UnhandledPromiseRejection: TypeError: Cannot read properties of undefined (reading 'DEALSTAGE')"," at process.<anonymous> (file:///var/runtime/index.mjs:1276:17)"," at process.emit (node:events:513:28)"," at emit (node:internal/process/promises:140:20)"," at processPromiseRejections (node:internal/process/promises:274:27)"," at processTicksAndRejections (node:internal/process/task_queues:97:32)"]}
Unknown application error occurred
Runtime.Unknown

Memory: 67/128 MB
Runtime: 84.48 ms

My code is as follows:

// Importing necessary library
const axios = require('axios');

// Base URL for HubSpot's APIs
const HUBSPOT_API_BASE_URL = 'https://api.hubapi.com';

function logEvent(message, data) {
 console.log(JSON.stringify({
 timestamp: new Date().toISOString(),
 message,
 data
 }));
}

// Centralized error handling function
function handleError(error, context = {}) {
 logEvent('Error occurred', {
 message: error.message,
 stack: error.stack,
 responseData: error.response ? { 
 data: error.response.data, 
 status: error.response.status,
 headers: error.response.headers,
 config: error.config 
 } : null,
 ...context // Additional context data
 });
}

 // Function to retrieve stage details from HubSpot
async function getDealstageDetails(pipelineId, stageId, API_KEY) {
 if (!pipelineId || !stageId) {
 logEvent('PipelineId or StageId is undefined or null');
 throw new Error('Invalid input parameters');
 }

 // Ensure the objectType is 'deals'
 const objectType = 'deals';
 const url = `${HUBSPOT_API_BASE_URL}/crm/v3/pipelines/${objectType}/${pipelineId}`;
 const config = {
 headers: {
 'Authorization': `Bearer ${API_KEY}`
 }
 };

 try {
 logEvent('Sending request to retrieve pipeline details', { url, config });
 const response = await axios.get(url, config);
 logEvent('Response received for retrieving pipeline details', { responseData: response.data });

 // Find the desired stage from the response
 const stageDetails = response.data.stages.find(stage => stage.stageId === stageId);

 if(!stageDetails) {
 logEvent('Stage not found in the pipeline', { stageId });
 throw new Error('Stage not found');
 }

 return stageDetails;
 } catch (error) {
 handleError(error, { url, config });
 throw error;
 }
 }

async function processDeal(deal, pipelineId, dealstage, lineItems, API_KEY) {
 logEvent('Processing deal', { dealId: deal.objectId });
 logEvent('Dealstage details', { pipeline: pipelineId, stage: dealstage });
 logEvent('Type of dealstage', { type: typeof dealstage }); // Log the data type of dealstage

 // Coerce dealstage to a string for the comparison
 if (pipelineId === '57968926' && String(dealstage) === '115192911') {
 logEvent('Processing deal in Impact EMS Wordpress Sync pipeline, Completed stage');
 logEvent('Total line items', { lineItemCount: lineItems.length });

 for (const [index, lineItem] of lineItems.entries()) {
 logEvent('Processing line item', { lineItemIndex: index, lineItemId: lineItem.id, lineItemSKU: lineItem.sku });

 try {
 const classObj = await findClassBySKU(lineItem.sku, API_KEY);

 if (classObj) {
 logEvent('Class found for SKU', { classObjId: classObj.id });
 await processLineItem(lineItem, deal, API_KEY);
 logEvent('Line item processed successfully', { lineItemIndex: index, lineItemId: lineItem.id });
 } else {
 logEvent('No class found for SKU', { lineItemSKU: lineItem.sku });
 }
 } catch (error) {
 logEvent('Error processing line item', { lineItemIndex: index, lineItemId: lineItem.id, error: error.toString(), stack: error.stack });
 }
 }
 } else {
 logEvent('Deal not in Impact EMS Wordpress Sync pipeline or not in Completed stage', { pipelineId, dealstage: String(dealstage) }); // Coerce dealstage to a string for logging
 }
}

 // Function to process a line item
async function processLineItem(lineItem, deal, API_KEY) {
 logEvent('INFO: Processing line item', { lineItemId: lineItem.id, lineItemSKU: lineItem.sku }); // Modified logEvent

 // Removing the condition that checks for 'in-person' in the SKU
 logEvent('INFO: Calling findClassBySKU', { lineItemSKU: lineItem.sku }); // Modified logEvent
 const classObj = await findClassBySKU(lineItem.sku, API_KEY);
 logEvent('INFO: Returned from findClassBySKU', { classObj }); // Modified logEvent
 if (classObj) {
 await createAssociations(deal, classObj, API_KEY);
 } else {
 logEvent('No class object returned for SKU', { lineItemSKU: lineItem.sku }); // Logging when no class object is found
 }
}

 // Function to create associations
async function createAssociations(deal, classObj, API_KEY) {
 logEvent('Class object found', { classObjId: classObj.id });

 try {
 logEvent('Attempting to create Deal to Class association', { dealId: deal.objectId, classObjId: classObj.id }); // Log attempt to create association
 const dealToClassResponse = await createAssociation({type: 'deals', id: deal.objectId}, {type: 'classes', id: classObj.id}, 'Deal to Class', API_KEY);
 logEvent('Response for creating Deal to Class association', { responseData: dealToClassResponse });

 logEvent('Attempting to create Contact to Class association', { contactId: deal.contact.id, classObjId: classObj.id }); // Log attempt to create association
 const contactToClassResponse = await createAssociation({type: 'contacts', id: deal.contact.id}, {type: 'classes', id: classObj.id}, 'Contact to Class', API_KEY);
 logEvent('Response for creating Contact to Class association', { responseData: contactToClassResponse });

 if (deal.company) {
 logEvent('Attempting to create Company to Class association', { companyId: deal.company.id, classObjId: classObj.id }); // Log attempt to create association
 const companyToClassResponse = await createAssociation({type: 'companies', id: deal.company.id}, {type: 'classes', id: classObj.id}, 'Company to Class', API_KEY);
 logEvent('Response for creating Company to Class association', { responseData: companyToClassResponse });
 }

 const trainingSessions = await findTrainingSessionsByClass(classObj, API_KEY);

 for (const session of trainingSessions) {
 logEvent('Attempting to create Contact to Training Session association', { contactId: deal.contact.id, sessionId: session.id }); // Log attempt to create association
 const contactToSessionResponse = await createAssociation({type: 'contacts', id: deal.contact.id}, {type: 'training-sessions', id: session.id}, 'Contact to Training Session', API_KEY);
 logEvent('Response for creating Contact to Training Session association', { responseData: contactToSessionResponse });
 }
 } catch (error) {
 logEvent('Error in createAssociations', { error: error.toString(), stack: error.stack });
 throw error; // re-throw the error after logging it
 }
}

async function initiateAndProcessDeal(event, API_KEY) {
 const deal = event.object;
 logEvent('INFO', 'Deal object', { deal: JSON.stringify(deal, null, 2) });
 const pipelineId = process.env.PIPELINE_ID;
 const dealstageId = event.inputFields.DEALSTAGE; // Adjusted this line

 try {
 // Get deal stage details
 const dealstage = await getDealstageDetails(pipelineId, dealstageId, API_KEY);

 const lineItems = await retrieveLineItems(deal.objectId, API_KEY);

 logEvent('Processing deal', { dealId: deal.objectId });
 logEvent('Dealstage details', { pipeline: pipelineId, stage: dealstage });
 logEvent('Type of dealstage', { type: typeof dealstage });

 logEvent('Processing deal in Impact EMS Wordpress Sync pipeline, Completed stage');
 logEvent('Total line items', { lineItemCount: lineItems.length });

 for (const [index, lineItem] of lineItems.entries()) {
 logEvent('Processing line item', { lineItemIndex: index, lineItemId: lineItem.id, lineItemSKU: lineItem.sku });

 try {
 const classObj = await findClassBySKU(lineItem.sku, API_KEY);

 if (classObj) {
 logEvent('Class found for SKU', { classObjId: classObj.id });
 await processLineItem(lineItem, deal, API_KEY);
 logEvent('Line item processed successfully', { lineItemIndex: index, lineItemId: lineItem.id });
 } else {
 logEvent('No class found for SKU', { lineItemSKU: lineItem.sku });
 }
 } catch (error) {
 logEvent('Error processing line item', { lineItemIndex: index, lineItemId: lineItem.id, error: error.toString(), stack: error.stack });
 }
 }
 } catch (error) {
 handleError(error);
 }
}

 // Function to retrieve line items associated with a deal using CRM Associations API
async function retrieveLineItems(dealId, API_KEY) {
 // URL for CRM Associations API to retrieve associated line items for a deal
 const url = `${HUBSPOT_API_BASE_URL}/crm/v3/associations/deals/line_item/batch/read`;

 // Data payload for the POST request
 const data = {
 inputs: [{ id: dealId }] // Providing the dealId to retrieve associated line items
 };

 // Configuration for the axios request
 const config = {
 headers: {
 'Authorization': `Bearer ${API_KEY}`,
 'Content-Type': 'application/json'
 }
 };

 // Log the request details
 logEvent('Request Details', {
 url: url,
 method: 'POST',
 headers: config.headers,
 data: data
 });

 try {
 logEvent('Sending request to retrieve line items', { url, data });
 const response = await axios.post(url, data, config); // Sending POST request to CRM Associations API
 logEvent('Response received for retrieving line items', { responseData: response.data });

 // Assuming that the response contains the associated line items in a property named 'results'
 return response.data.results; 
 } catch (error) {
 handleError(error, { url, config }); // Adjusted to use centralized error handling function
 throw error;
 }
}

// findClassBySKU function
async function findClassBySKU(sku, API_KEY) {
 if (!sku) {
 logEvent('SKU is undefined or null');
 return null;
 }
 const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/classes/search`; 
 const data = {
 filterGroups: [{
 filters: [{
 value: sku,
 propertyName: 'sku',
 operator: 'EQ'
 }]
 }]
 };
 const config = {
 headers: {
 'Authorization': `Bearer ${API_KEY}`,
 'Content-Type': 'application/json'
 }
 };

 try {
 logEvent('Sending request to find class by SKU', { url, data });
 const response = await axios.post(url, data, config);
 logEvent('Response received for finding class by SKU', { responseData: response.data });
 return response.data.results[0]; // Assuming each SKU corresponds to at most one class
 } catch (error) {
 logEvent('Error finding class by SKU', { error: error.toString(), stack: error.stack });
 throw error;
 }
}

 // exports.main function
exports.main = async (event, callback) => {
 const startTime = Date.now();
 logEvent('Event received', { event });

 const API_KEY = process.env.billing_contacts_secret;
 const deal = event.object;

 logEvent('Deal object', { deal });

 if (!deal.objectId) {
 logEvent('deal.id is undefined');
 return callback(new Error('deal.id is undefined'));
 }

 // Initiate SKU search and process the deal
 await initiateAndProcessDeal(deal, API_KEY);

 callback();

 const endTime = Date.now();
 logEvent('Operation completed', { durationMs: endTime - startTime });
};

 // createAssociation function
async function createAssociation(object1, object2, associationType, API_KEY) {
 const url = `${HUBSPOT_API_BASE_URL}/crm/v4/objects/${object1.type}/${object1.id}/associations/${object2.type}/${object2.id}/default`;
 const config = {
 method: 'PUT', // Updated method to PUT as per v4 documentation
 headers: {
 'Authorization': `Bearer ${API_KEY}`
 }
 };

 try {
 logEvent('Sending request to create association', { url, config });
 const response = await axios(url, config);
 logEvent('Response received for creating association', { responseData: response.data });
 return response.data;
 } catch (error) {
 logEvent('Error creating association', { error: error.toString(), stack: error.stack });
 throw error;
 }
}

// checkExistingAssociation function
async function checkExistingAssociation(object1, object2, associationType, API_KEY) {
 const url = `${HUBSPOT_API_BASE_URL}/crm/v3/associations/${object1.type}/${object2.type}`; 
 const config = {
 headers: {
 'Authorization': 'Bearer ${API_KEY}'
 },
 params: {
 id: object1.id
 }
 };

 try {
 logEvent('Sending request to check existing association', { url, config });
 const response = await axios.get(url, config);
 logEvent('Response received for checking existing association', { responseData: response.data });
 return response.data.results.some(association => association.id === object2.id && association.type === associationType);
 } catch (error) {
 logEvent('Error checking existing association', { error: error.toString(), stack: error.stack });
 throw error;
 }
}

 // findTrainingSessionsByClass function
async function findTrainingSessionsByClass(classObj, API_KEY) {
 const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/training-sessions/search`; 
 const data = {
 filterGroups: [{
 filters: [{
 propertyName: 'classId',
 operator: 'EQ',
 value: classObj.id
 }]
 }]
 };
 const config = {
 headers: {
 'Authorization': 'Bearer ${API_KEY}'
 }
 };

 try {
 logEvent('Sending request to find training sessions by class', { url, data });
 const response = await axios.post(url, data, config);
 logEvent('Response received for finding training sessions by class', { responseData: response.data });
 return response.data.results;
 } catch (error) {
 logEvent('Error finding training sessions by class', { error: error.toString(), stack: error.stack });
 throw error;
 }
}

Thank you

Hi, @ah8 :waving_hand: Thanks for sharing your code and the logs. It looks like one issue is with the function initiateAndProcessDeal

Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"TypeError: Cannot read properties of undefined (reading 'DEALSTAGE')"}

Have you tried adding a check there?

if (someObject && someObject.fields) {
 let dealStage = someObject.fields.DEALSTAGE;
} else {
 console.error("someObject or someObject.fields is undefined");
}

Thanks! — Jaycee