Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ async function postLeadToCrm(payload) {
};
const res = await fetch(crmIntegration.crm_webhook_url.trim(), { method: 'POST', headers, body: JSON.stringify(body) });
const text = await res.text();
if (!res.ok) throw new Error(`HubSpot contact create failed ${res.status}: ${text.slice(0, 500)}`);
if (!res.ok) {
const err = new Error(`HubSpot contact create failed ${res.status}: ${text.slice(0, 500)}`);
err.statusCode = res.status;
throw err;
}
console.log('[crm-lead-sample] HubSpot contact create success; status=', res.status);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ module.exports = {
/**
* Handles customer_created events
* @param {import('../types/types').HandlerPayload} payload - Event and iparams
* @returns {Promise<import('../types/types').HandlerResult | void>}
*/
customerCreatedHandler: async function (payload) {
try {
await postLeadToCrm(payload);
console.log('Lead posted to CRM successfully');
} catch (err) {
throw new Error(err.message);
// 409: contact already exists in HubSpot β€” no value in retrying.
if (err.statusCode === 409) {
return {
statusCode: 409,
body: JSON.stringify({ message: 'Contact already exists in HubSpot; skipping duplicate lead creation' }),
};
}
// Throw to signal a transient failure β€” the platform WILL retry (treated as 5xx).
throw err;
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,23 @@ export interface HandlerPayload {
/** Installation parameter values grouped by section */
iparams: IparamInputs;
}

/**
* Optional return value from a handler function.
*
* Return this when the handler wants to signal an application-level error
* WITHOUT triggering a platform retry (e.g. invalid input, business rule
* violation). Throwing an exception causes the platform to retry the event;
* returning a HandlerResult with statusCode >= 400 does not.
*
* If the handler returns nothing (or undefined), statusCode defaults to 200.
*/
export interface HandlerResult {
/**
* HTTP status code (100–599). Defaults to 200 if omitted.
* Return 4xx to signal a non-retryable application error.
*/
statusCode?: number;
/** Optional response body (e.g. a JSON-encoded error message). */
body?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async function scheduleAdvanceInvoice(payload, site, apiKey) {
message: err.message,
subscription_id: eventContent.subscription.id,
});
return;
return { statusCode: status, body: JSON.stringify({ message: err.message, api_error_code: err.api_error_code }) };
}
throw err;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ module.exports = {
/**
* On `subscription_created`, configures an advance invoice schedule from iparams.
* @param {import('../types/types.d.ts').HandlerPayload} payload - Event and iparams
* @returns {Promise<import('../types/types.d.ts').HandlerResult | void>}
*/
subscriptionCreatedHandler: async function (payload) {
console.log('Processing subscription_created event');
const scheduleType = payload.iparams?.advance_invoice_configuration?.schedule_type;
// Return 4xx to signal a non-retryable error β€” the platform will NOT retry.
if (scheduleType !== 'fixed' && scheduleType !== 'specific') {
return {
statusCode: 400,
body: JSON.stringify({ message: `Unsupported schedule_type: "${scheduleType}". Expected "fixed" or "specific".` }),
};
}
try {
const site = process.env['CB_APPS_SITE_DOMAIN'];
const apiKey = process.env['CB_APPS_READ_WRITE_API'];
await scheduleAdvanceInvoice(payload, site, apiKey);
return await scheduleAdvanceInvoice(payload, site, apiKey);
} catch (error) {
throw new Error(error.message);
// Throw to signal a transient failure β€” the platform WILL retry (treated as 5xx).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
throw error;
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,23 @@ export interface HandlerPayload {
/** Installation parameter values grouped by section */
iparams: IparamInputs;
}

/**
* Optional return value from a handler function.
*
* Return this when the handler wants to signal an application-level error
* WITHOUT triggering a platform retry (e.g. invalid input, business rule
* violation). Throwing an exception causes the platform to retry the event;
* returning a HandlerResult with statusCode >= 400 does not.
*
* If the handler returns nothing (or undefined), statusCode defaults to 200.
*/
export interface HandlerResult {
/**
* HTTP status code (100–599). Defaults to 200 if omitted.
* Return 4xx to signal a non-retryable application error.
*/
statusCode?: number;
/** Optional response body (e.g. a JSON-encoded error message). */
body?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,33 @@ module.exports = {
/**
* Must match `manifest.json` β†’ `events.invoice_updated.handler`.
* @param {import('../types/types').HandlerPayload} payload
* @returns {Promise<import('../types/types').HandlerResult | void>}
*/
invoiceUpdatedHandler: async function (payload) {
const eventContent = payload.event.content;
const dunningStatus = eventContent?.invoice?.dunning_status || eventContent?.transaction?.dunning_status;
// Non-exhausted events are not actionable β€” ack silently without retrying.
if (dunningStatus !== 'exhausted') {
return;
}
// Mirrors the fallback in handleInvoiceUpdated: invoice.subscription_id, then subscription.id.
const subscriptionId = eventContent?.invoice?.subscription_id || eventContent?.subscription?.id;
// One-time invoices are not linked to a subscription β€” nothing to pause or cancel.
// Return 4xx to signal a non-retryable skip β€” the platform will NOT retry.
if (!subscriptionId) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Exhausted dunning invoice has no linked subscription; skipping dunning action' }),
};
}
try {
const site = String(process.env['CB_APPS_SITE_DOMAIN'] || '').trim();
const apiKey = process.env['CB_APPS_READ_WRITE_API'];
await handleInvoiceUpdated(payload, site, apiKey);
console.log('Dunning exhaustion handled successfully');
} catch (error) {
throw new Error(error.message);
// Throw to signal a transient failure β€” the platform WILL retry (treated as 5xx).
throw error;
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,24 @@ export interface EventRecord {
*/
export interface HandlerPayload {
event: EventRecord;
}

/**
* Optional return value from a handler function.
*
* Return this when the handler wants to signal an application-level error
* WITHOUT triggering a platform retry (e.g. invalid input, business rule
* violation). Throwing an exception causes the platform to retry the event;
* returning a HandlerResult with statusCode >= 400 does not.
*
* If the handler returns nothing (or undefined), statusCode defaults to 200.
*/
export interface HandlerResult {
/**
* HTTP status code (100–599). Defaults to 200 if omitted.
* Return 4xx to signal a non-retryable application error.
*/
statusCode?: number;
/** Optional response body (e.g. a JSON-encoded error message). */
body?: string;
}
Loading