diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ad10f58..a8b8400 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,14 +7,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ["14", "15", "16", "17", "18"] + node: ["14", "15", "16", "17", "18" ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: node_modules key: customerio-${{ matrix.node }}-${{ hashFiles('package-lock.json') }} diff --git a/lib/api.ts b/lib/api.ts index 1c6e8bf..dd7408e 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,7 +1,7 @@ import type { RequestOptions } from 'https'; import Request, { BearerAuth, RequestData } from './request'; import { Region, RegionUS } from './regions'; -import { SendEmailRequest, SendPushRequest } from './api/requests'; +import { SendEmailRequest, SendPushRequest, SendSMSRequest } from './api/requests'; import { cleanEmail, isEmpty, isIdentifierType, MissingParamError } from './utils'; import { Filter, IdentifierType } from './types'; @@ -85,6 +85,14 @@ export class APIClient { return this.request.post(`${this.apiRoot}/send/push`, req.message); } + sendSMS(req: SendSMSRequest) { + if (!(req instanceof SendSMSRequest)) { + throw new Error('"request" must be an instance of SendSMSRequest'); + } + + return this.request.post(`${this.apiRoot}/send/sms`, req.message); + } + getCustomersByEmail(email: string) { if (typeof email !== 'string' || isEmpty(email)) { throw new Error('"email" must be a string'); @@ -160,4 +168,4 @@ export class APIClient { } } -export { SendEmailRequest, SendPushRequest } from './api/requests'; +export { SendEmailRequest, SendPushRequest, SendSMSRequest } from './api/requests'; diff --git a/lib/api/requests.ts b/lib/api/requests.ts index 9623e49..104390d 100644 --- a/lib/api/requests.ts +++ b/lib/api/requests.ts @@ -167,3 +167,40 @@ export class SendPushRequest { } } } + +export type SMSMessage = Partial; + +export type SendSMSRequestRequiredOptions = { + identifiers: Identifiers; + transactional_message_id: string | number; +}; + +export type SendSMSRequestOptionalOptions = Partial<{ + to: string; + disable_message_retention: boolean; + send_to_unsubscribed: boolean; + queue_draft: boolean; + message_data: Record; + send_at: number; + language: string; +}>; + +export type SendSMSRequestOptions = SendSMSRequestRequiredOptions & SendSMSRequestOptionalOptions & {}; + +export class SendSMSRequest { + message: SMSMessage; + + constructor(opts: SendSMSRequestOptions) { + this.message = { + identifiers: opts.identifiers, + to: opts.to, + transactional_message_id: opts.transactional_message_id, + disable_message_retention: opts.disable_message_retention, + send_to_unsubscribed: opts.send_to_unsubscribed, + queue_draft: opts.queue_draft, + message_data: opts.message_data, + send_at: opts.send_at, + language: opts.language, + }; + } +} diff --git a/test/api.ts b/test/api.ts index 787201d..63bfde3 100644 --- a/test/api.ts +++ b/test/api.ts @@ -6,6 +6,7 @@ import { DeliveryExportRequestOptions, SendEmailRequest, SendPushRequest, + SendSMSRequest, } from '../lib/api'; import { RegionUS, RegionEU } from '../lib/regions'; import { Filter, IdentifierType } from '../lib/types'; @@ -499,3 +500,68 @@ test('#getAttributes: success with type email', (t) => { ), ); }); + +test('sendSMS: passing in a plain object throws an error', (t) => { + sinon.stub(t.context.client.request, 'post'); + + let req = { identifiers: { id: '2' }, transactional_message_id: 1 }; + + t.throws(() => t.context.client.sendSMS(req as any), { + message: /"request" must be an instance of SendSMSRequest/, + }); + t.falsy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/sms`)); +}); + +test('#sendSMS: with template: success', (t) => { + sinon.stub(t.context.client.request, 'post'); + let req = new SendSMSRequest({ + to: '+1234567890', + identifiers: { id: '2' }, + transactional_message_id: 1, + }); + t.context.client.sendSMS(req); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/sms`, req.message)); + t.is(req.message.transactional_message_id, 1); + t.is(req.message.to, '+1234567890'); +}); + +test('#sendSMS: with optional parameters: success', (t) => { + sinon.stub(t.context.client.request, 'post'); + let req = new SendSMSRequest({ + to: '+1234567890', + identifiers: { id: '2' }, + transactional_message_id: 1, + message_data: { key: 'value' }, + disable_message_retention: true, + send_to_unsubscribed: true, + queue_draft: true, + send_at: 1234567890, + language: 'en', + }); + t.context.client.sendSMS(req); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/sms`, req.message)); + t.is(req.message.transactional_message_id, 1); + t.is(req.message.to, '+1234567890'); + t.deepEqual(req.message.message_data, { key: 'value' }); + t.true(req.message.disable_message_retention); + t.true(req.message.send_to_unsubscribed); + t.true(req.message.queue_draft); + t.is(req.message.send_at, 1234567890); + t.is(req.message.language, 'en'); +}); + +test('#sendSMS: error', async (t) => { + sinon.stub(t.context.client.request, 'post').rejects({ message: 'sample error', statusCode: 400 }); + + let req = new SendSMSRequest({ + to: '+1234567890', + identifiers: { id: '2' }, + transactional_message_id: 1, + }); + t.context.client.sendSMS(req).catch((err) => { + t.is(err.message, 'sample error'); + t.is(err.statusCode, 400); + }); + + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/sms`, req.message)); +});