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/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/__tests__/sms.test.ts b/src/__tests__/sms.test.ts index 82b4d05..28c2698 100644 --- a/src/__tests__/sms.test.ts +++ b/src/__tests__/sms.test.ts @@ -176,4 +176,55 @@ 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..7640976 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,14 +69,15 @@ 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) { 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 @@ -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); + } }