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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions integration/test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -142,6 +143,7 @@ async function main(): Promise<void> {
'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) {
Expand All @@ -163,6 +165,7 @@ async function main(): Promise<void> {
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)}`;
Expand Down Expand Up @@ -846,6 +849,23 @@ async function main(): Promise<void> {
);
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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
51 changes: 51 additions & 0 deletions src/__tests__/sms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
44 changes: 42 additions & 2 deletions src/sms/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type SMSCampaign = {
message: string;
title: string;
senderPhone?: string;
templateId?: number;
};

export type SMSResponse = {
Expand Down Expand Up @@ -68,14 +69,15 @@ export class SMS {
message: string,
title: string,
senderPhone?: string,
options?: SMSOptions
options?: SMSOptions,
templateId?: number
): Promise<SMSResponse> {
// 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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<SMSResponse> {
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<SMSResponse> {
const account: Account = { firstName, lastName, phone };
return this.sendWithTemplate([account], templateId, title, senderPhone, options);
}
}
Loading