From b3da232208015031d84ea5a3a0146ca8c13ed1c1 Mon Sep 17 00:00:00 2001 From: cb-jananivijayan Date: Mon, 27 Jul 2026 22:00:43 +0530 Subject: [PATCH 1/2] FRAMENGG-13161 support app error --- .../handler/crmLead.js | 6 +++++- .../handler/handler.js | 11 +++++++++- .../types/types.d.ts | 20 +++++++++++++++++++ .../handler/handler.js | 10 ++++++++++ .../types/types.d.ts | 20 +++++++++++++++++++ .../handler/handler.js | 11 ++++++++++ .../types/types.d.ts | 20 +++++++++++++++++++ 7 files changed, 96 insertions(+), 2 deletions(-) diff --git a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/crmLead.js b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/crmLead.js index 58cc8a4..d19cd6e 100644 --- a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/crmLead.js +++ b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/crmLead.js @@ -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); } diff --git a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/handler.js b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/handler.js index 598289f..42f90c5 100644 --- a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/handler.js +++ b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/handler/handler.js @@ -11,13 +11,22 @@ module.exports = { /** * Handles customer_created events * @param {import('../types/types').HandlerPayload} payload - Event and iparams + * @returns {Promise} */ 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; } }, }; diff --git a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/types/types.d.ts b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/types/types.d.ts index 9e91d56..8f5b17f 100644 --- a/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/types/types.d.ts +++ b/sample-apps/sample-apps-with-iparams/crm_lead_on_customer_created_sample_app/types/types.d.ts @@ -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; +} diff --git a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js index 857da76..ffa8081 100644 --- a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js +++ b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js @@ -11,14 +11,24 @@ 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} */ 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); } catch (error) { + // Throw to signal a transient failure — the platform WILL retry (treated as 5xx). throw new Error(error.message); } }, diff --git a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/types/types.d.ts b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/types/types.d.ts index 122ab77..f3e8022 100644 --- a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/types/types.d.ts +++ b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/types/types.d.ts @@ -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; +} diff --git a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js index 0068612..e5f07ab 100644 --- a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js +++ b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js @@ -10,14 +10,25 @@ module.exports = { /** * Must match `manifest.json` → `events.invoice_updated.handler`. * @param {import('../types/types').HandlerPayload} payload + * @returns {Promise} */ invoiceUpdatedHandler: async function (payload) { + const subscriptionId = payload.event.content?.invoice?.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: 'Invoice is not linked to a 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 to signal a transient failure — the platform WILL retry (treated as 5xx). throw new Error(error.message); } }, diff --git a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/types/types.d.ts b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/types/types.d.ts index 634b497..b199ae7 100644 --- a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/types/types.d.ts +++ b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/types/types.d.ts @@ -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; } \ No newline at end of file From f8424153a18e46402a978b384f7a9868e50044f1 Mon Sep 17 00:00:00 2001 From: cb-jananivijayan Date: Mon, 27 Jul 2026 22:31:25 +0530 Subject: [PATCH 2/2] fix --- .../handler/advanceInvoiceSchedule.js | 2 +- .../handler/handler.js | 4 ++-- .../handler/handler.js | 13 ++++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/advanceInvoiceSchedule.js b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/advanceInvoiceSchedule.js index f82160f..a2a34e1 100644 --- a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/advanceInvoiceSchedule.js +++ b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/advanceInvoiceSchedule.js @@ -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; } diff --git a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js index ffa8081..42d85f8 100644 --- a/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js +++ b/sample-apps/sample-apps-with-iparams/schedule_advance_invoice_sample_app/handler/handler.js @@ -26,10 +26,10 @@ module.exports = { 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 to signal a transient failure — the platform WILL retry (treated as 5xx). - throw new Error(error.message); + throw error; } }, }; diff --git a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js index e5f07ab..cb2c285 100644 --- a/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js +++ b/sample-apps/sample-apps-without-iparams/dunning_cycle_end_handler_sample_app/handler/handler.js @@ -13,13 +13,20 @@ module.exports = { * @returns {Promise} */ invoiceUpdatedHandler: async function (payload) { - const subscriptionId = payload.event.content?.invoice?.subscription_id; + 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: 'Invoice is not linked to a subscription; skipping dunning action' }), + body: JSON.stringify({ message: 'Exhausted dunning invoice has no linked subscription; skipping dunning action' }), }; } try { @@ -29,7 +36,7 @@ module.exports = { console.log('Dunning exhaustion handled successfully'); } catch (error) { // Throw to signal a transient failure — the platform WILL retry (treated as 5xx). - throw new Error(error.message); + throw error; } }, };