From 8bd1c2b08338c9f864c74d35478ef9fe45feb283 Mon Sep 17 00:00:00 2001 From: Mike Alvarez Date: Mon, 17 Aug 2026 20:00:22 -0500 Subject: [PATCH 1/3] https://mobileaws.atlassian.net/browse/CLOUD-2885 Adding support for the new API changes to force accounts to always use templates when sending SMS messages --- README.md | 28 ++++++++++++++++++++++++ src/__tests__/sms.test.ts | 45 +++++++++++++++++++++++++++++++++++++++ src/sms/sms.ts | 42 +++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1cb25c..210ab02 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,34 @@ ccai.sms.sendSingle( .catch(error => console.error('Error:', error)); ``` +### SMS — Template-Controlled Accounts + +If an account has been configured to enforce template-only messaging, all campaigns must reference a pre-approved template ID. Sending a free-text message to such an account will result in a `422` error. + +```typescript +// Send to multiple recipients using a template +ccai.sms.sendWithTemplate( + accounts, + 12345, // templateId — the ID of the approved template + "My Campaign" +) + .then(response => console.log('Success:', response)) + .catch(error => console.error('Error:', error)); + +// Send to a single recipient using a template +ccai.sms.sendSingleWithTemplate( + "John", + "Doe", + "+15551234567", + 12345, // templateId + "My Campaign" +) + .then(response => console.log('Success:', response)) + .catch(error => console.error('Error:', error)); +``` + +The message body is resolved server-side from the template. Variable substitution (e.g. `${firstName}`) is applied automatically using the recipient's account data. + ### MMS ```typescript diff --git a/src/__tests__/sms.test.ts b/src/__tests__/sms.test.ts index 82b4d05..9283b89 100644 --- a/src/__tests__/sms.test.ts +++ b/src/__tests__/sms.test.ts @@ -176,4 +176,49 @@ describe('SMS Service', () => { await expect(sms.sendSingle('John', 'Doe', '', 'Hello', 'Test')).rejects.toThrow(); }); }); + + describe('sendWithTemplate()', () => { + it('should send SMS with templateId and empty message', async () => { + const result = await sms.sendWithTemplate([validAccount], 12345, 'Template Campaign'); + + expect(mockRequest).toHaveBeenCalledWith( + 'post', + '/clients/client-123/campaigns/direct', + expect.objectContaining({ + accounts: [validAccount], + message: '', + title: 'Template Campaign', + templateId: 12345, + }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should include templateId when passed to send()', async () => { + await sms.send([validAccount], '', 'Test', undefined, undefined, 99); + + expect(mockRequest).toHaveBeenCalledWith( + 'post', + '/clients/client-123/campaigns/direct', + expect.objectContaining({ templateId: 99 }) + ); + }); + }); + + describe('sendSingleWithTemplate()', () => { + it('should send SMS to a single recipient with a templateId', async () => { + const result = await sms.sendSingleWithTemplate('John', 'Doe', '+15551234567', 12345, 'Template Campaign'); + + expect(mockRequest).toHaveBeenCalledWith( + 'post', + '/clients/client-123/campaigns/direct', + expect.objectContaining({ + accounts: [{ firstName: 'John', lastName: 'Doe', phone: '+15551234567' }], + templateId: 12345, + title: 'Template Campaign', + }) + ); + expect(result).toEqual(mockResponse); + }); + }); }); diff --git a/src/sms/sms.ts b/src/sms/sms.ts index 5d1b2d6..e95b90c 100644 --- a/src/sms/sms.ts +++ b/src/sms/sms.ts @@ -13,6 +13,7 @@ export type SMSCampaign = { message: string; title: string; senderPhone?: string; + templateId?: number; }; export type SMSResponse = { @@ -68,7 +69,8 @@ export class SMS { message: string, title: string, senderPhone?: string, - options?: SMSOptions + options?: SMSOptions, + templateId?: number ): Promise { // Validate inputs if (!accounts || !Array.isArray(accounts) || accounts.length === 0) { @@ -112,6 +114,10 @@ export class SMS { campaignData.senderPhone = senderPhone; } + if (templateId !== undefined) { + campaignData.templateId = templateId; + } + try { // Notify progress if callback provided if (options?.onProgress) { @@ -168,4 +174,38 @@ export class SMS { return this.send([account], message, title, senderPhone, options); } + + /** + * Send an SMS using a pre-approved template (for template-controlled accounts) + * @param accounts - Array of recipient objects + * @param templateId - ID of the approved template to use + * @param title - Campaign title + * @param senderPhone - Optional sender phone number + * @param options - Optional settings + */ + async sendWithTemplate( + accounts: Account[], + templateId: number, + title: string, + senderPhone?: string, + options?: SMSOptions + ): Promise { + return this.send(accounts, '', title, senderPhone, options, templateId); + } + + /** + * Send an SMS to a single recipient using a pre-approved template + */ + async sendSingleWithTemplate( + firstName: string, + lastName: string, + phone: string, + templateId: number, + title: string, + senderPhone?: string, + options?: SMSOptions + ): Promise { + const account: Account = { firstName, lastName, phone }; + return this.sendWithTemplate([account], templateId, title, senderPhone, options); + } } From aed02cbf53895527cd4b1b4c62c75bbb8a669e77 Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Tue, 18 Aug 2026 14:57:08 -0500 Subject: [PATCH 2/3] feat: added a new integration test for jenkins pipeline --- integration/test.ts | 24 ++++++++++++++++++++++-- package.json | 2 +- src/sms/sms.ts | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/integration/test.ts b/integration/test.ts index d28ca1d..35ce04f 100644 --- a/integration/test.ts +++ b/integration/test.ts @@ -1,7 +1,8 @@ /** - * Node.js SDK integration tests — 52 tests + * Node.js SDK integration tests — 54 tests * Covers: SMS (1-6), MMS (7-17), Email (18-22), Webhook (23-29), Contact (30-31), - * Brands (32-36), Campaigns (37-42), ContactValidator (43-46), Negative cases (47-52) + * Brands (32-36), Campaigns (37-42), ContactValidator (43-46), Negative cases (47-52), + * SMS Templates (53-54) * * Test results use three states: * PASS — the test ran and all assertions held @@ -142,6 +143,7 @@ async function main(): Promise { 'CCAI_TEST_FIRST_NAME_3', 'CCAI_TEST_LAST_NAME_3', 'WEBHOOK_URL', + 'CCAI_TEST_TEMPLATE_ID', ]; const missing = REQUIRED_ENV.filter((key) => !process.env[key]); if (missing.length > 0) { @@ -163,6 +165,7 @@ async function main(): Promise { const lastName2 = process.env.CCAI_TEST_LAST_NAME_2!; const firstName3 = process.env.CCAI_TEST_FIRST_NAME_3!; const lastName3 = process.env.CCAI_TEST_LAST_NAME_3!; + const templateId = Number(process.env.CCAI_TEST_TEMPLATE_ID); // Unique per-run suffix so parallel SDK runs don't collide on the same webhook URL const runId = `node-${Date.now().toString(36)}`; @@ -846,6 +849,23 @@ async function main(): Promise { ); assertSendResponse(resp); }); + + console.log('\n--- SMS Templates ---'); + + await run('53 SMS.sendWithTemplate', async () => { + const resp = await client.sms.sendWithTemplate( + [ + { firstName: firstName1, lastName: lastName1, phone: phone1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2 }, + ], templateId, 'Node Template Test' + ); + assertSendResponse(resp); + }); + + await run('54 SMS.sendSingleWithTemplate', async () => { + const resp = await client.sms.sendSingleWithTemplate(firstName1, lastName1, phone1, templateId, 'Node Single Template Test'); + assertSendResponse(resp); + }); } finally { // ── Cleanup ───────────────────────────────────────────────────────────────── // Always runs, even if the test body threw: delete leftover resources and the temp PNG. diff --git a/package.json b/package.json index a4bf973..bda97b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccai-node", - "version": "1.1.0", + "version": "1.2.0", "description": "TypeScript client for CloudContactAI API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/sms/sms.ts b/src/sms/sms.ts index e95b90c..7640976 100644 --- a/src/sms/sms.ts +++ b/src/sms/sms.ts @@ -77,7 +77,7 @@ export class SMS { throw new Error('At least one account is required'); } - if (!message) throw new Error('Message is required'); + if (!message && templateId === undefined) throw new Error('Message is required'); if (!title) throw new Error('Campaign title is required'); // Validate each account has the required fields From fc3e390e2497fd75d583e3f722adcfce82c3033c Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Wed, 9 Sep 2026 16:48:43 -0500 Subject: [PATCH 3/3] fix: lint validation --- src/__tests__/sms.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/__tests__/sms.test.ts b/src/__tests__/sms.test.ts index 9283b89..28c2698 100644 --- a/src/__tests__/sms.test.ts +++ b/src/__tests__/sms.test.ts @@ -207,7 +207,13 @@ describe('SMS Service', () => { describe('sendSingleWithTemplate()', () => { it('should send SMS to a single recipient with a templateId', async () => { - const result = await sms.sendSingleWithTemplate('John', 'Doe', '+15551234567', 12345, 'Template Campaign'); + const result = await sms.sendSingleWithTemplate( + 'John', + 'Doe', + '+15551234567', + 12345, + 'Template Campaign' + ); expect(mockRequest).toHaveBeenCalledWith( 'post',