diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..88c1eb1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +node_modules/ +coverage/ +dist/ +examples/ +*.md +LICENSE +.env +.env.* +*.log +*.jpg +*.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7c12a6..ef81d4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,3 @@ jobs: cache: 'npm' - run: npm ci - run: npm run test:coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/ - fail_ci_if_error: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..94c1460 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish to NPM + +on: + push: + tags: + - 'release/v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + + - run: npm ci + - run: npm run build + - run: npm test + + - name: Publish to NPM + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 4e836c1..33353b8 100644 --- a/.gitignore +++ b/.gitignore @@ -86,10 +86,11 @@ coverage/ # Test files image.jpg -# Real API tests — local only # Real API tests — local only src/test_real.ts dist/test_real.js dist/test_real.d.ts test_real.ts test-progress.ts + +.claude/ diff --git a/README.md b/README.md index e2bde1b..647821d 100644 --- a/README.md +++ b/README.md @@ -71,51 +71,200 @@ const ccai = new CCAI({ apiKey: 'API-KEY-TOKEN' }); -// Define a progress callback -const trackProgress = (status: string) => { - console.log(`Progress: ${status}`); -}; +// ── Option A: All-in-one (recommended) ───────────────────────────────────── +// sendWithImage handles upload + send in one call +const account: Account = { firstName: 'John', lastName: 'Doe', phone: '+15551234567' }; + +const response = await ccai.mms.sendWithImage( + 'path/to/image.jpg', // local image path + 'image/jpeg', // content type + [account], // recipients + 'Hello ${firstName}, check out this image!', + 'MMS Campaign', + // optional: senderPhone?: string + // optional: options?: SMSOptions + // optional: forceNewCampaign?: boolean (default true) +); +console.log(`MMS sent! Campaign ID: ${response.campaignId}`); + +// ── Option B: Manual workflow (step-by-step) ──────────────────────────────── + +// Step 1 — Get a pre-signed S3 upload URL +const { signedS3Url, fileKey } = await ccai.mms.getSignedUploadUrl( + 'image.jpg', // fileName + 'image/jpeg' // fileType + // optional: fileBasePath?: string + // optional: publicFile?: boolean +); + +// Step 2 — Upload the image directly to S3 +const uploaded = await ccai.mms.uploadImageToSignedUrl( + signedS3Url, + 'path/to/image.jpg', + 'image/jpeg' +); + +// Step 3 — (Optional) Confirm file is available +const stored = await ccai.mms.checkFileUploaded(fileKey); +console.log('File URL:', stored?.url); + +// Step 4a — Send to multiple recipients using the uploaded fileKey +const bulkResponse = await ccai.mms.send( + fileKey, + [account], + 'Hello ${firstName}!', + 'MMS Campaign' + // optional: senderPhone?: string + // optional: options?: SMSOptions + // optional: forceNewCampaign?: boolean +); + +// Step 4b — Send to a single recipient +const singleResponse = await ccai.mms.sendSingle( + fileKey, + 'John', + 'Doe', + '+15551234567', + 'Hello ${firstName}!', + 'MMS Campaign' + // optional: customData?: string + // optional: senderPhone?: string + // optional: options?: SMSOptions + // optional: forceNewCampaign?: boolean +); -// Create options with progress tracking +// ── Progress tracking ──────────────────────────────────────────────────────── const options: SMSOptions = { timeout: 60000, - onProgress: trackProgress + retries: 3, + onProgress: (status: string) => console.log(`Progress: ${status}`) }; +``` -// Complete MMS workflow (get URL, upload image, send MMS) -async function sendMmsWithImage() { - try { - // Path to your image file - const imagePath = 'path/to/your/image.jpg'; - const contentType = 'image/jpeg'; - - // Define recipient - const account: Account = { - firstName: 'John', - lastName: 'Doe', - phone: '+15551234567' - }; - - // Send MMS with image in one step - const response = await ccai.mms.sendWithImage( - imagePath, - contentType, - [account], - "Hello ${firstName}, check out this image!", - "MMS Campaign Example", - options - ); - - console.log(`MMS sent! Campaign ID: ${response.campaignId}`); - } catch (error) { - console.error('Error sending MMS:', error); - } -} +### Brands + +Register and manage brands for TCR (The Campaign Registry) business verification. + +```typescript +import { CCAI } from 'ccai-node'; + +const ccai = new CCAI({ + clientId: 'YOUR-CLIENT-ID', + apiKey: 'API-KEY-TOKEN' +}); + +// Create a brand +const brand = await ccai.brands.create({ + legalCompanyName: 'Collect.org Inc.', + dba: 'Collect', + entityType: 'NON_PROFIT', + taxId: '123456789', + taxIdCountry: 'US', + country: 'US', + verticalType: 'NON_PROFIT', + websiteUrl: 'https://www.collect.org', + street: '123 Main Street', + city: 'San Francisco', + state: 'CA', + postalCode: '94105', + contactFirstName: 'Jane', + contactLastName: 'Doe', + contactEmail: 'jane@collect.org', + contactPhone: '+14155551234' +}); +console.log('Brand created:', brand.id); + +// Get a brand by ID +const fetched = await ccai.brands.get(brand.id); +console.log('Website match score:', fetched.websiteMatchScore); + +// List all brands for the account +const brands = await ccai.brands.list(); +console.log(`Found ${brands.length} brand(s)`); + +// Update a brand (partial update) +const updated = await ccai.brands.update(brand.id, { + street: '456 Oak Avenue', + city: 'Los Angeles' +}); + +// Delete a brand +await ccai.brands.delete(brand.id); +``` + +#### Entity Types + +`PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT`, `SOLE_PROPRIETOR` + +> Note: `PUBLIC_PROFIT` entities require `stockSymbol` and `stockExchange` fields. + +#### Vertical Types + +`AUTOMOTIVE`, `AGRICULTURE`, `BANKING`, `COMMUNICATION`, `CONSTRUCTION`, `EDUCATION`, `ENERGY`, `ENTERTAINMENT`, `GOVERNMENT`, `HEALTHCARE`, `HOSPITALITY`, `INSURANCE`, `LEGAL`, `MANUFACTURING`, `NON_PROFIT`, `PROFESSIONAL`, `REAL_ESTATE`, `RETAIL`, `TECHNOLOGY`, `TRANSPORTATION` + +### Campaigns + +Register and manage campaigns for TCR (The Campaign Registry) carrier vetting. Each campaign must be linked to a verified brand. + +```typescript +import { CCAI } from 'ccai-node'; + +const ccai = new CCAI({ + clientId: 'YOUR-CLIENT-ID', + apiKey: 'API-KEY-TOKEN' +}); + +// Create a campaign +const campaign = await ccai.campaigns.create({ + brandId: 1, + useCase: 'MIXED', + subUseCases: ['CUSTOMER_CARE', 'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION'], + description: 'Security codes and support messaging.', + messageFlow: 'Users opt-in via signup form at https://example.com/signup', + hasEmbeddedLinks: true, + hasEmbeddedPhone: false, + isAgeGated: false, + isDirectLending: false, + optInKeywords: ['START'], + optInMessage: 'Welcome! Reply STOP to cancel.', + optInProofUrl: 'https://example.com/opt-in-proof.png', + helpKeywords: ['HELP'], + helpMessage: 'For HELP email support@example.com.', + optOutKeywords: ['STOP'], + optOutMessage: 'STOP received. You are unsubscribed.', + sampleMessages: [ + 'Your code is 554321. Reply STOP to cancel.', + 'Your ticket has been updated. Reply HELP for info.' + ] +}); +console.log('Campaign created:', campaign.id); + +// Get a campaign by ID +const fetchedCampaign = await ccai.campaigns.get(campaign.id); -// Call the function -sendMmsWithImage(); +// List all campaigns for the account +const campaigns = await ccai.campaigns.list(); +console.log(`Found ${campaigns.length} campaign(s)`); + +// Update a campaign (partial update) +const updatedCampaign = await ccai.campaigns.update(campaign.id, { + description: 'Updated description.' +}); + +// Delete a campaign +await ccai.campaigns.delete(campaign.id); ``` +#### Use Cases + +`TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `FRAUD_ALERT`, `HIGHER_EDUCATION`, `LOW_VOLUME_MIXED`, `MARKETING`, `MIXED`, `POLLING_VOTING`, `PUBLIC_SERVICE_ANNOUNCEMENT`, `SECURITY_ALERT` + +> Note: `MIXED` and `LOW_VOLUME_MIXED` campaigns require 2–3 `subUseCases`. + +#### Sub-Use Cases + +`TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `FRAUD_ALERT`, `MARKETING`, `POLLING_VOTING` + ### Email ```typescript @@ -132,11 +281,12 @@ const response = await ccai.email.sendSingle( 'Doe', 'john@example.com', 'Welcome to Our Service', - '

Hello ${firstName},

Thank you for signing up!

', - 'noreply@yourcompany.com', - 'support@yourcompany.com', - 'Your Company', - 'Welcome Email' + '

Hello ${firstName},

Thank you for signing up!

', // htmlContent (message) + undefined, // textContent: plain-text alternative (optional) + 'noreply@yourcompany.com', // senderEmail (optional, defaults to noreply@cloudcontactai.com) + 'support@yourcompany.com', // replyEmail (optional) + 'Your Company', // senderName (optional) + 'Welcome Email' // title (optional) ); console.log('Email sent:', response); @@ -181,6 +331,43 @@ await ccai.contact.setDoNotText(false, undefined, '+15551234567'); await ccai.contact.setDoNotText(true, 'contact-abc-123'); ``` +### Contact Validator + +Validate email addresses and phone numbers. + +```typescript +import { CCAI } from 'ccai-node'; + +const ccai = new CCAI({ + clientId: 'YOUR-CLIENT-ID', + apiKey: 'YOUR-API-KEY' +}); + +// Validate a single email +const emailResult = await ccai.contactValidator.validateEmail('user@example.com'); +console.log(emailResult.status); // "valid" | "invalid" | "risky" +console.log(emailResult.metadata.safe_to_send); // true | false + +// Validate multiple emails (up to 50) +const bulkEmails = await ccai.contactValidator.validateEmails([ + 'user@example.com', + 'invalid@nonexistent.xyz' +]); +console.log(bulkEmails.summary); // { total: 2, valid: 1, invalid: 1, risky: 0 } + +// Validate a single phone number +const phoneResult = await ccai.contactValidator.validatePhone('+15551234567', 'US'); +console.log(phoneResult.status); // "valid" | "invalid" | "landline" +console.log(phoneResult.metadata.carrier_type); // "mobile" | "landline" | "voip" + +// Validate multiple phone numbers (up to 50) +const bulkPhones = await ccai.contactValidator.validatePhones([ + { phone: '+15551234567' }, + { phone: '+15559876543', countryCode: 'US' } +]); +console.log(bulkPhones.summary); // { total: 2, valid: 1, invalid: 0, risky: 0, landline: 1 } +``` + ### Webhooks CloudContactAI can send webhook notifications when certain events occur, such as when messages are sent or received. Use the Webhook service to register, manage, and verify webhooks programmatically. @@ -199,6 +386,9 @@ const ccai = new CCAI({ const webhookConfig: WebhookConfig = { url: 'https://your-app.com/api/ccai-webhook', // secret is optional - if not provided, server generates one automatically + // method?: string (default 'POST') + // integrationType?: string (e.g. 'REST') + // events?: WebhookEventType[] }; const webhook = await ccai.webhook.register(webhookConfig); console.log('Webhook registered with ID:', webhook.id); @@ -252,10 +442,16 @@ console.log('Message:', event.data.Message); #### Webhook Events -CloudContactAI currently supports the following webhook events: +CloudContactAI supports the following webhook event types (available via `WebhookEventType` enum): -1. **Message Sent (Outbound)** - Triggered when a message is sent from your CloudContactAI account -2. **Message Received (Inbound)** - Triggered when a message is received by your CloudContactAI account +| Event | Value | Description | +|---|---|---| +| `MESSAGE_SENT` | `message.sent` | Outbound message sent from your account | +| `MESSAGE_RECEIVED` | `message.received` | Inbound message received by your account | +| `MESSAGE_INCOMING` | `message.incoming` | Incoming message before processing | +| `MESSAGE_EXCLUDED` | `message.excluded` | Message excluded (e.g. opted-out contact) | +| `MESSAGE_ERROR_CARRIER` | `message.error.carrier` | Carrier-side delivery error | +| `MESSAGE_ERROR_CLOUDCONTACT` | `message.error.cloudcontact` | Platform-side delivery error | #### Event Payload Schema @@ -542,7 +738,10 @@ This project includes a `.gitignore` file that excludes: - Send SMS to single or multiple recipients - Send MMS with images (automatic upload to S3) - Send Email campaigns with HTML content to single or multiple recipients +- Brand registration and management for TCR verification +- Campaign registration and management for TCR carrier vetting - Manage contact opt-out preferences (setDoNotText) +- Validate email addresses (valid/invalid/risky) and phone numbers (valid/invalid/landline) - Webhook management: register, list, update, delete - Webhook signature verification (HMAC-SHA256) - Next.js API route handlers for webhook events diff --git a/integration/Dockerfile b/integration/Dockerfile new file mode 100644 index 0000000..b9d0786 --- /dev/null +++ b/integration/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine + +WORKDIR /sdk +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +COPY tsconfig.json ./ +COPY src/ ./src/ +RUN npm run build + +WORKDIR /sdk/integration +COPY integration/package.json ./ +RUN npm install + +COPY integration/ ./ + +CMD ["npx", "ts-node", "--project", "tsconfig.json", "test.ts"] diff --git a/integration/Dockerfile.release b/integration/Dockerfile.release new file mode 100644 index 0000000..3c6d377 --- /dev/null +++ b/integration/Dockerfile.release @@ -0,0 +1,20 @@ +FROM node:20-alpine + +ARG SDK_VERSION=1.0.2 + +# Install the published SDK from npm into /sdk +WORKDIR /sdk +RUN npm install ccai-node@${SDK_VERSION} + +# test.ts uses `require('../dist/index.js')` — symlink so that path resolves +# to the installed package's compiled output +RUN ln -s /sdk/node_modules/ccai-node/dist /sdk/dist + +# Set up the integration test runner (ts-node, typescript, etc.) +WORKDIR /sdk/integration +COPY integration/package.json ./ +RUN npm install + +COPY integration/ ./ + +CMD ["npx", "ts-node", "--project", "tsconfig.json", "test.ts"] diff --git a/integration/package.json b/integration/package.json new file mode 100644 index 0000000..3a63e7e --- /dev/null +++ b/integration/package.json @@ -0,0 +1,16 @@ +{ + "name": "ccai-test-node", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "ts-node --project tsconfig.json test.ts" + }, + "dependencies": { + "axios": "^1.6.7" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/integration/test.ts b/integration/test.ts new file mode 100644 index 0000000..7036cba --- /dev/null +++ b/integration/test.ts @@ -0,0 +1,634 @@ +/** + * Node.js SDK integration tests — 42 tests + * Covers: SMS (1-6), MMS (7-17), Email (18-22), Webhook (23-29), Contact (30-31), Brands (32-36), Campaigns (37-42) + */ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +// Import from the locally built SDK +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { CCAI } = require('../dist/index.js'); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +let passed = 0; +let failed = 0; + +async function run(name: string, fn: () => Promise): Promise { + try { + await fn(); + console.log(` PASS [${name}]`); + passed++; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.log(` FAIL [${name}]: ${msg}`); + failed++; + } +} + +function mustEnv(key: string): string { + const val = process.env[key]; + if (!val) { + console.error(`ERROR: required env var ${key} is not set`); + process.exit(2); + } + return val; +} + +function hmacSHA256Base64(secret: string, message: string): string { + return crypto.createHmac('sha256', secret).update(message).digest('base64'); +} + +function writeTempPNG(): string { + const pngB64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=='; + const buf = Buffer.from(pngB64, 'base64'); + const tmpPath = path.join(os.tmpdir(), `ccai_test_${Date.now()}.png`); + fs.writeFileSync(tmpPath, buf); + return tmpPath; +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +async function main(): Promise { + // Validate required env vars + const clientId = mustEnv('CCAI_CLIENT_ID'); + const apiKey = mustEnv('CCAI_API_KEY'); + const phone1 = mustEnv('CCAI_TEST_PHONE'); + const phone2 = mustEnv('CCAI_TEST_PHONE_2'); + const phone3 = mustEnv('CCAI_TEST_PHONE_3'); + const email1 = mustEnv('CCAI_TEST_EMAIL'); + const email2 = mustEnv('CCAI_TEST_EMAIL_2'); + const email3 = mustEnv('CCAI_TEST_EMAIL_3'); + const firstName1 = mustEnv('CCAI_TEST_FIRST_NAME'); + const lastName1 = mustEnv('CCAI_TEST_LAST_NAME'); + const firstName2 = mustEnv('CCAI_TEST_FIRST_NAME_2'); + const lastName2 = mustEnv('CCAI_TEST_LAST_NAME_2'); + const firstName3 = mustEnv('CCAI_TEST_FIRST_NAME_3'); + const lastName3 = mustEnv('CCAI_TEST_LAST_NAME_3'); + const webhookURL = mustEnv('WEBHOOK_URL'); + + // Create client — use CCAI_BASE_URL if set (local dev), otherwise fall back to test environment + const client = new CCAI({ + clientId, + apiKey, + useTestEnvironment: !process.env.CCAI_BASE_URL, + }); + + console.log('=============================================='); + console.log(' CCAI Node.js SDK Integration Tests'); + console.log('=============================================='); + + // Write temp PNG for MMS tests + const pngPath = writeTempPNG(); + + // ── SMS Tests (1-6) ────────────────────────────────────────────────────────── + console.log('\n--- SMS ---'); + + // 01 — SMS.sendSingle + await run('01 SMS.sendSingle', async () => { + await client.sms.sendSingle(firstName1, lastName1, phone1, 'Hello from Node SDK!', 'Node Test'); + }); + + // 02 — SMS.send (1 recipient) + await run('02 SMS.send (1 recipient)', async () => { + await client.sms.send( + [{ firstName: firstName1, lastName: lastName1, phone: phone1 }], + 'Hello 1 recipient!', + 'Node Test' + ); + }); + + // 03 — SMS.send (2 recipients) + await run('03 SMS.send (2 recipients)', async () => { + await client.sms.send( + [ + { firstName: firstName1, lastName: lastName1, phone: phone1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2 }, + ], + 'Hello 2 recipients!', + 'Node Test' + ); + }); + + // 04 — SMS.send (3 recipients) + await run('04 SMS.send (3 recipients)', async () => { + await client.sms.send( + [ + { firstName: firstName1, lastName: lastName1, phone: phone1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2 }, + { firstName: firstName3, lastName: lastName3, phone: phone3 }, + ], + 'Hello 3 recipients!', + 'Node Test' + ); + }); + + // 05 — SMS.send with data + await run('05 SMS.send with data', async () => { + await client.sms.send( + [ + { + firstName: firstName1, + lastName: lastName1, + phone: phone1, + data: { city: 'Miami', offer: '20% off' }, + }, + ], + 'Hello from ${city}! Claim your ${offer}.', + 'Node Test Data' + ); + }); + + // 06 — SMS.send with messageData + await run('06 SMS.send with messageData', async () => { + await client.sms.send( + [ + { + firstName: firstName1, + lastName: lastName1, + phone: phone1, + customData: '{"trackingId":"abc123"}', + }, + ], + 'Hello with messageData!', + 'Node Test MsgData' + ); + }); + + // ── MMS Tests (7-17) ───────────────────────────────────────────────────────── + console.log('\n--- MMS ---'); + + let signedUrlResp: { signedS3Url: string; fileKey: string } | null = null; + let mmsDepFailed = false; + + // 07 — MMS.getSignedUploadUrl + await run('07 MMS.getSignedUploadUrl', async () => { + const resp = await client.mms.getSignedUploadUrl('test_image.png', 'image/png', undefined, true); + if (!resp.signedS3Url) { + mmsDepFailed = true; + throw new Error('signedS3Url is empty'); + } + signedUrlResp = resp; + }); + + // 08 — MMS.uploadImageToSignedUrl + await run('08 MMS.uploadImageToSignedUrl', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + const ok = await client.mms.uploadImageToSignedUrl(signedUrlResp.signedS3Url, pngPath, 'image/png'); + if (!ok) throw new Error('upload returned false'); + }); + + // 09 — MMS.sendSingle + await run('09 MMS.sendSingle', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.sendSingle(signedUrlResp.fileKey, firstName1, lastName1, phone1, 'MMS single!', 'Node MMS Test'); + }); + + // 10 — MMS.send (1 recipient) + await run('10 MMS.send (1 recipient)', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.send( + signedUrlResp.fileKey, + [{ firstName: firstName1, lastName: lastName1, phone: phone1 }], + 'MMS 1 recipient!', + 'Node MMS Test' + ); + }); + + // 11 — MMS.send (2 recipients) + await run('11 MMS.send (2 recipients)', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.send( + signedUrlResp.fileKey, + [ + { firstName: firstName1, lastName: lastName1, phone: phone1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2 }, + ], + 'MMS 2 recipients!', + 'Node MMS Test' + ); + }); + + // 12 — MMS.send (3 recipients) + await run('12 MMS.send (3 recipients)', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.send( + signedUrlResp.fileKey, + [ + { firstName: firstName1, lastName: lastName1, phone: phone1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2 }, + { firstName: firstName3, lastName: lastName3, phone: phone3 }, + ], + 'MMS 3 recipients!', + 'Node MMS Test' + ); + }); + + // 13 — MMS.send with data + await run('13 MMS.send with data', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.send( + signedUrlResp.fileKey, + [{ firstName: firstName1, lastName: lastName1, phone: phone1, data: { product: 'Widget' } }], + 'Check out ${product}!', + 'Node MMS Data' + ); + }); + + // 14 — MMS.send with messageData + await run('14 MMS.send with messageData', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.send( + signedUrlResp.fileKey, + [ + { + firstName: firstName1, + lastName: lastName1, + phone: phone1, + customData: '{"campaignId":"mms-test-001"}', + }, + ], + 'MMS with messageData!', + 'Node MMS MsgData' + ); + }); + + // 15 — MMS.checkFileUploaded + await run('15 MMS.checkFileUploaded', async () => { + if (mmsDepFailed || !signedUrlResp) throw new Error('dependency test 07 failed'); + await client.mms.checkFileUploaded(signedUrlResp.fileKey); + }); + + // 16 — MMS.sendWithImage (fresh upload) + await run('16 MMS.sendWithImage (fresh upload)', async () => { + if (mmsDepFailed) throw new Error('dependency test 07 failed'); + await client.mms.sendWithImage( + pngPath, + 'image/png', + [{ firstName: firstName1, lastName: lastName1, phone: phone1 }], + 'MMS with image!', + 'Node MMS Image', + undefined, + undefined, + true + ); + }); + + // 17 — MMS.sendWithImage (cached) + await run('17 MMS.sendWithImage (cached)', async () => { + if (mmsDepFailed) throw new Error('dependency test 07 failed'); + await client.mms.sendWithImage( + pngPath, + 'image/png', + [{ firstName: firstName1, lastName: lastName1, phone: phone1 }], + 'MMS cached image!', + 'Node MMS Cache', + undefined, + undefined, + true + ); + }); + + // ── Email Tests (18-22) ────────────────────────────────────────────────────── + console.log('\n--- Email ---'); + + const senderEmail = 'noreply@cloudcontactai.com'; + const senderName = 'CCAI Test'; + const replyEmail = 'noreply@cloudcontactai.com'; + + // 18 — Email.sendSingle + await run('18 Email.sendSingle', async () => { + // sendSingle(firstName, lastName, email, subject, message, textContent?, senderEmail?, replyEmail?, senderName?, title?) + await client.email.sendSingle( + firstName1, lastName1, email1, + 'Node SDK Test Email', + '

Hello from Node SDK!

', + undefined, + senderEmail, replyEmail, senderName, + 'Node Email Test' + ); + }); + + // 19 — Email.send (1 recipient) + await run('19 Email.send (1 recipient)', async () => { + await client.email.send( + [{ firstName: firstName1, lastName: lastName1, phone: phone1, email: email1 }], + 'Node SDK Email 1', + '

Hello 1!

', + senderEmail, replyEmail, senderName, + 'Node Email Test' + ); + }); + + // 20 — Email.send (2 recipients) + await run('20 Email.send (2 recipients)', async () => { + await client.email.send( + [ + { firstName: firstName1, lastName: lastName1, phone: phone1, email: email1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2, email: email2 }, + ], + 'Node SDK Email 2', + '

Hello 2!

', + senderEmail, replyEmail, senderName, + 'Node Email Test' + ); + }); + + // 21 — Email.send (3 recipients) + await run('21 Email.send (3 recipients)', async () => { + await client.email.send( + [ + { firstName: firstName1, lastName: lastName1, phone: phone1, email: email1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2, email: email2 }, + { firstName: firstName3, lastName: lastName3, phone: phone3, email: email3 }, + ], + 'Node SDK Email 3', + '

Hello 3!

', + senderEmail, replyEmail, senderName, + 'Node Email Test' + ); + }); + + // 22 — Email.sendCampaign (direct campaign object) + await run('22 Email.sendCampaign', async () => { + const campaign = { + subject: 'Node SDK Campaign Test', + title: 'Node Email Campaign', + message: '

Campaign email from Node SDK!

', + senderEmail, + replyEmail, + senderName, + accounts: [ + { firstName: firstName1, lastName: lastName1, phone: phone1, email: email1 }, + { firstName: firstName2, lastName: lastName2, phone: phone2, email: email2 }, + ], + campaignType: 'EMAIL' as const, + addToList: 'noList', + contactInput: 'accounts', + fromType: 'single', + senders: [], + }; + await client.email.sendCampaign(campaign); + }); + + // ── Webhook Tests (23-29) ──────────────────────────────────────────────────── + console.log('\n--- Webhook ---'); + + const secret = 'test-webhook-secret-node'; + let registeredWebhookId = ''; + + // 23 — Webhook.register + await run('23 Webhook.register', async () => { + const resp = await client.webhook.register({ url: webhookURL, secret }); + const id = resp?.id; + if (!id) throw new Error('webhook ID is empty after register'); + registeredWebhookId = String(id); + }); + + // 24 — Webhook.list + await run('24 Webhook.list', async () => { + const hooks = await client.webhook.list(); + if (!Array.isArray(hooks) || hooks.length === 0) + throw new Error('expected at least one webhook, got 0'); + }); + + // 25 — Webhook.update + await run('25 Webhook.update', async () => { + if (!registeredWebhookId) throw new Error('no webhook ID from test 23'); + await client.webhook.update(registeredWebhookId, { + url: webhookURL + '?updated=1', + secret: 'updated-secret-node', + }); + }); + + // 26 — Webhook.verifySignature (valid) + await run('26 Webhook.verifySignature (valid)', async () => { + const eventHash = 'abc123eventHash'; + const sig = hmacSHA256Base64(secret, `${clientId}:${eventHash}`); + const ok = client.webhook.verifySignature(sig, clientId, eventHash, secret); + if (!ok) throw new Error('expected valid signature to return true'); + }); + + // 27 — Webhook.verifySignature (invalid) + await run('27 Webhook.verifySignature (invalid)', async () => { + const ok = client.webhook.verifySignature('invalidsig==', clientId, 'somehash', secret); + if (ok) throw new Error('expected invalid signature to return false'); + }); + + // 28 — Webhook.parseEvent + await run('28 Webhook.parseEvent', async () => { + const payload = JSON.stringify({ + eventType: 'message.sent', + data: { to: '+15005550001' }, + eventHash: 'abc123', + }); + const event = client.webhook.parseEvent(payload); + if (!event.eventType) throw new Error('eventType is empty after parseEvent'); + }); + + // 29 — Webhook.delete + await run('29 Webhook.delete', async () => { + if (!registeredWebhookId) throw new Error('no webhook ID from test 23'); + await client.webhook.delete(registeredWebhookId); + }); + + // ── Contact Tests (30-31) ──────────────────────────────────────────────────── + console.log('\n--- Contact ---'); + + // 30 — Contact.setDoNotText(true) + await run('30 Contact.setDoNotText(true)', async () => { + await client.contact.setDoNotText(true, undefined, phone1); + }); + + // 31 — Contact.setDoNotText(false) + await run('31 Contact.setDoNotText(false)', async () => { + await client.contact.setDoNotText(false, undefined, phone1); + }); + + // ── Brand Tests (32-36) ────────────────────────────────────────────────────── + console.log('\n--- Brands ---'); + + let brandId: number | null = null; + + // 32 — Brands.create + await run('32 Brands.create', async () => { + const resp = await client.brands.create({ + legalCompanyName: 'Test Company LLC', + entityType: 'PRIVATE_PROFIT', + taxId: '123456789', + taxIdCountry: 'US', + country: 'US', + verticalType: 'TECHNOLOGY', + websiteUrl: 'https://example.com', + street: '123 Main St', + city: 'Miami', + state: 'FL', + postalCode: '33101', + contactFirstName: firstName1, + contactLastName: lastName1, + contactEmail: email1, + contactPhone: phone1, + }); + if (!resp?.id) throw new Error('Invalid brand id'); + brandId = resp.id; + }); + + // 33 — Brands.get + await run('33 Brands.get', async () => { + if (!brandId) throw new Error('dependency test 32 failed'); + const resp = await client.brands.get(brandId); + if (resp?.id !== brandId) throw new Error('Brand id mismatch'); + }); + + // 34 — Brands.list + await run('34 Brands.list', async () => { + const resp = await client.brands.list(); + if (!Array.isArray(resp)) throw new Error('Expected an array'); + }); + + // 35 — Brands.update + await run('35 Brands.update', async () => { + if (!brandId) throw new Error('dependency test 32 failed'); + const resp = await client.brands.update(brandId, { city: 'Orlando' }); + if (resp?.id !== brandId) throw new Error('Brand id mismatch after update'); + }); + + // 36 — Brands.delete + await run('36 Brands.delete', async () => { + if (!brandId) throw new Error('dependency test 32 failed'); + await client.brands.delete(brandId); + }); + + // ── Campaign Tests (37-42) ──────────────────────────────────────────────────── + console.log('\n--- Campaigns ---'); + + let campaignBrandId: number | null = null; + let campaignId: number | null = null; + + // 37 — Campaign setup: create brand + await run('37 Campaign setup — Brands.create', async () => { + const resp = await client.brands.create({ + legalCompanyName: 'Campaign Test LLC', + entityType: 'PRIVATE_PROFIT', + taxId: '987654321', + taxIdCountry: 'US', + country: 'US', + verticalType: 'TECHNOLOGY', + websiteUrl: 'https://example.com', + street: '456 Test Ave', + city: 'Miami', + state: 'FL', + postalCode: '33101', + contactFirstName: firstName1, + contactLastName: lastName1, + contactEmail: email1, + contactPhone: phone1, + }); + if (!resp?.id) throw new Error('Invalid brand id'); + campaignBrandId = resp.id; + }); + + // 38 — Campaigns.create + await run('38 Campaigns.create', async () => { + if (!campaignBrandId) throw new Error('dependency test 37 failed'); + const resp = await client.campaigns.create({ + brandId: campaignBrandId, + useCase: 'MARKETING', + description: 'Integration test campaign for automated testing', + messageFlow: 'Customers opt-in via website form at https://example.com/sms-signup', + hasEmbeddedLinks: false, + hasEmbeddedPhone: false, + isAgeGated: false, + isDirectLending: false, + optInKeywords: ['START', 'YES'], + optInMessage: 'You have opted in to receive messages. Reply STOP to unsubscribe.', + optInProofUrl: 'https://example.com/opt-in-proof', + helpKeywords: ['HELP', 'INFO'], + helpMessage: 'For help reply HELP or call 1-800-555-0000.', + optOutKeywords: ['STOP', 'END'], + optOutMessage: 'You have been unsubscribed. Reply START to opt back in. STOP', + sampleMessages: [ + 'Hello ${firstName}, this is a test message. Reply STOP to unsubscribe.', + 'Reminder: your appointment is tomorrow. Reply HELP for assistance.', + ], + }); + if (!resp?.id) throw new Error('Invalid campaign id'); + campaignId = resp.id; + }); + + // 39 — Campaigns.get + await run('39 Campaigns.get', async () => { + if (!campaignId) throw new Error('dependency test 38 failed'); + const resp = await client.campaigns.get(campaignId); + if (resp?.id !== campaignId) throw new Error('Campaign id mismatch'); + }); + + // 40 — Campaigns.list + await run('40 Campaigns.list', async () => { + const resp = await client.campaigns.list(); + if (!Array.isArray(resp)) throw new Error('Expected an array'); + }); + + // 41 — Campaigns.update + await run('41 Campaigns.update', async () => { + if (!campaignId) throw new Error('dependency test 38 failed'); + const resp = await client.campaigns.update(campaignId, { + description: 'Updated integration test campaign description', + }); + if (resp?.id !== campaignId) throw new Error('Campaign id mismatch after update'); + }); + + // 42 — Campaigns.delete + cleanup brand + await run('42 Campaigns.delete', async () => { + if (!campaignId) throw new Error('dependency test 38 failed'); + await client.campaigns.delete(campaignId); + if (campaignBrandId) await client.brands.delete(campaignBrandId); + }); + + // ── Contact Validator ──────────────────────────────────────────────────────── + + // 43 — ContactValidator.validateEmail + await run('43 ContactValidator.validateEmail', async () => { + const resp = await client.contactValidator.validateEmail(email1); + if (!resp?.status) throw new Error('status is empty'); + }); + + // 44 — ContactValidator.validateEmails + await run('44 ContactValidator.validateEmails', async () => { + const resp = await client.contactValidator.validateEmails([email1, email2]); + if (resp?.summary?.total !== 2) throw new Error(`expected summary.total=2, got ${resp?.summary?.total}`); + }); + + // 45 — ContactValidator.validatePhone + await run('45 ContactValidator.validatePhone', async () => { + const resp = await client.contactValidator.validatePhone(phone1); + if (!resp?.status) throw new Error('status is empty'); + }); + + // 46 — ContactValidator.validatePhones + await run('46 ContactValidator.validatePhones', async () => { + const resp = await client.contactValidator.validatePhones([{ phone: phone1 }, { phone: phone2 }]); + if (resp?.summary?.total !== 2) throw new Error(`expected summary.total=2, got ${resp?.summary?.total}`); + }); + + // ── Cleanup & Results ───────────────────────────────────────────────────────── + fs.unlinkSync(pngPath); + + console.log('\n=============================================='); + console.log(` RESULTS: ${passed} passed, ${failed} failed`); + console.log('=============================================='); + + const summary = JSON.stringify({ sdk: 'node', passed, failed, total: passed + failed }); + console.log(`\nSUMMARY_JSON: ${summary}`); + + process.exit(failed > 0 ? 1 : 0); +} + +main().catch((err) => { + console.error('Unexpected error:', err); + process.exit(2); +}); diff --git a/integration/tsconfig.json b/integration/tsconfig.json new file mode 100644 index 0000000..23ca900 --- /dev/null +++ b/integration/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": false, + "skipLibCheck": true, + "outDir": "./dist" + }, + "include": ["test.ts"] +} diff --git a/package.json b/package.json index 1f89729..a4bf973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccai-node", - "version": "1.0.1", + "version": "1.1.0", "description": "TypeScript client for CloudContactAI API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/__tests__/brands.test.ts b/src/__tests__/brands.test.ts new file mode 100644 index 0000000..8a5fe49 --- /dev/null +++ b/src/__tests__/brands.test.ts @@ -0,0 +1,187 @@ +import { Brands } from '../brands/brands'; +import { CCAI } from '../ccai'; + +const mockCustomRequest = jest.fn(); + +const mockCcai = { + getClientId: () => 'client-123', + getApiKey: () => 'api-key-456', + getComplianceBaseUrl: () => 'https://compliance-test-cloudcontactai.allcode.com/api', + customRequest: mockCustomRequest, +} as unknown as CCAI; + +const validBrand = { + legalCompanyName: 'Test Corp', + entityType: 'PRIVATE_PROFIT', + taxId: '123456789', + taxIdCountry: 'US', + country: 'US', + verticalType: 'TECHNOLOGY', + websiteUrl: 'https://test.com', + street: '123 Main St', + city: 'Austin', + state: 'TX', + postalCode: '78701', + contactFirstName: 'John', + contactLastName: 'Doe', + contactEmail: 'john@test.com', + contactPhone: '+15551234567', +}; + +const mockResponse = { + id: 1, + accountId: 42, + ...validBrand, + websiteMatchScore: null, + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', +}; + +describe('Brands Service', () => { + let brands: Brands; + + beforeEach(() => { + jest.clearAllMocks(); + brands = new Brands(mockCcai); + mockCustomRequest.mockResolvedValue(mockResponse); + }); + + describe('create()', () => { + it('should create a brand with valid data', async () => { + const result = await brands.create(validBrand); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'post', + '/v1/brands', + validBrand, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw if required fields are missing', async () => { + await expect(brands.create({})).rejects.toThrow('Validation failed'); + }); + + it('should throw for invalid entityType', async () => { + await expect(brands.create({ ...validBrand, entityType: 'INVALID' })).rejects.toThrow( + 'Invalid entity type' + ); + }); + + it('should throw for invalid verticalType', async () => { + await expect(brands.create({ ...validBrand, verticalType: 'INVALID' })).rejects.toThrow( + 'Invalid vertical type' + ); + }); + + it('should throw for invalid taxIdCountry', async () => { + await expect(brands.create({ ...validBrand, taxIdCountry: 'XX' })).rejects.toThrow( + 'Invalid tax ID country' + ); + }); + + it('should throw for invalid websiteUrl', async () => { + await expect(brands.create({ ...validBrand, websiteUrl: 'not-a-url' })).rejects.toThrow( + 'Website URL must start with' + ); + }); + + it('should throw for invalid contactEmail', async () => { + await expect(brands.create({ ...validBrand, contactEmail: 'bad-email' })).rejects.toThrow( + 'Invalid email format' + ); + }); + + it('should throw for US taxId not 9 digits', async () => { + await expect(brands.create({ ...validBrand, taxId: '12345' })).rejects.toThrow( + 'Tax ID must be exactly 9 digits' + ); + }); + + it('should throw if PUBLIC_PROFIT missing stockSymbol', async () => { + await expect(brands.create({ ...validBrand, entityType: 'PUBLIC_PROFIT' })).rejects.toThrow( + 'Stock symbol is required' + ); + }); + + it('should throw for invalid stockExchange', async () => { + await expect( + brands.create({ + ...validBrand, + entityType: 'PUBLIC_PROFIT', + stockSymbol: 'TST', + stockExchange: 'INVALID', + }) + ).rejects.toThrow('Invalid stock exchange'); + }); + + it('should accept PUBLIC_PROFIT with stock fields', async () => { + const data = { + ...validBrand, + entityType: 'PUBLIC_PROFIT', + stockSymbol: 'TST', + stockExchange: 'NASDAQ', + }; + await brands.create(data); + expect(mockCustomRequest).toHaveBeenCalled(); + }); + }); + + describe('get()', () => { + it('should get a brand by ID', async () => { + const result = await brands.get(1); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'get', + '/v1/brands/1', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('list()', () => { + it('should list all brands', async () => { + mockCustomRequest.mockResolvedValue([mockResponse]); + const result = await brands.list(); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'get', + '/v1/brands', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual([mockResponse]); + }); + }); + + describe('update()', () => { + it('should update a brand with partial data', async () => { + const data = { street: '456 Oak Ave' }; + await brands.update(1, data); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'patch', + '/v1/brands/1', + data, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + }); + + it('should validate fields present in update', async () => { + await expect(brands.update(1, { entityType: 'INVALID' })).rejects.toThrow( + 'Invalid entity type' + ); + }); + }); + + describe('delete()', () => { + it('should delete a brand', async () => { + await brands.delete(1); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'delete', + '/v1/brands/1', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + }); + }); +}); diff --git a/src/__tests__/campaigns.test.ts b/src/__tests__/campaigns.test.ts new file mode 100644 index 0000000..969e8e5 --- /dev/null +++ b/src/__tests__/campaigns.test.ts @@ -0,0 +1,211 @@ +import { Campaigns } from '../campaigns/campaigns'; +import { CCAI } from '../ccai'; + +const mockCustomRequest = jest.fn(); + +const mockCcai = { + getClientId: () => 'client-123', + getApiKey: () => 'api-key-456', + getComplianceBaseUrl: () => 'https://compliance-test-cloudcontactai.allcode.com/api', + customRequest: mockCustomRequest, +} as unknown as CCAI; + +const validCampaign = { + brandId: 1, + useCase: 'MIXED', + subUseCases: ['CUSTOMER_CARE', 'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION'], + description: 'Test campaign', + messageFlow: 'Users opt-in via signup form at https://example.com/signup', + hasEmbeddedLinks: true, + hasEmbeddedPhone: false, + isAgeGated: false, + isDirectLending: false, + optInKeywords: ['START'], + optInMessage: 'Welcome! Reply STOP to cancel.', + optInProofUrl: 'https://example.com/opt-in.png', + helpKeywords: ['HELP'], + helpMessage: 'For HELP email support@example.com.', + optOutKeywords: ['STOP'], + optOutMessage: 'STOP received. You are unsubscribed.', + sampleMessages: [ + 'Your code is 554321. Reply STOP to cancel.', + 'Your ticket has been updated. Reply HELP for info.', + ], +}; + +const mockResponse = { + id: 1, + accountId: 42, + ...validCampaign, + monthlyFee: 10, + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', +}; + +describe('Campaigns Service', () => { + let campaigns: Campaigns; + + beforeEach(() => { + jest.clearAllMocks(); + campaigns = new Campaigns(mockCcai); + mockCustomRequest.mockResolvedValue(mockResponse); + }); + + describe('create()', () => { + it('should create a campaign with valid data', async () => { + const result = await campaigns.create(validCampaign); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'post', + '/v1/campaigns', + validCampaign, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw if required fields are missing', async () => { + await expect(campaigns.create({})).rejects.toThrow('Validation failed'); + }); + + it('should throw for invalid useCase', async () => { + await expect(campaigns.create({ ...validCampaign, useCase: 'INVALID' })).rejects.toThrow( + 'Invalid use case' + ); + }); + + it('should throw if MIXED has fewer than 2 subUseCases', async () => { + await expect( + campaigns.create({ ...validCampaign, subUseCases: ['CUSTOMER_CARE'] }) + ).rejects.toThrow('2-3 sub use cases'); + }); + + it('should throw if MIXED has invalid subUseCase', async () => { + await expect( + campaigns.create({ + ...validCampaign, + subUseCases: ['CUSTOMER_CARE', 'INVALID', 'MARKETING'], + }) + ).rejects.toThrow('Invalid sub use case'); + }); + + it('should throw if non-MIXED has subUseCases', async () => { + await expect( + campaigns.create({ + ...validCampaign, + useCase: 'MARKETING', + subUseCases: ['CUSTOMER_CARE', 'FRAUD_ALERT'], + }) + ).rejects.toThrow('subUseCases should be empty'); + }); + + it('should throw if sampleMessages has fewer than 2', async () => { + await expect(campaigns.create({ ...validCampaign, sampleMessages: ['one'] })).rejects.toThrow( + '2-5 items' + ); + }); + + it('should throw if sampleMessages missing STOP', async () => { + await expect( + campaigns.create({ + ...validCampaign, + sampleMessages: ['Hello Reply HELP for info.', 'Another Reply HELP msg'], + }) + ).rejects.toThrow('Reply STOP'); + }); + + it('should throw if sampleMessages missing HELP', async () => { + await expect( + campaigns.create({ + ...validCampaign, + sampleMessages: ['Reply STOP to cancel.', 'Another Reply STOP msg'], + }) + ).rejects.toThrow('Reply HELP'); + }); + + it('should throw if optOutMessage missing STOP keyword', async () => { + await expect( + campaigns.create({ ...validCampaign, optOutMessage: 'You are unsubscribed.' }) + ).rejects.toThrow('optOutMessage must contain'); + }); + + it('should throw if helpMessage missing HELP keyword', async () => { + await expect( + campaigns.create({ ...validCampaign, helpMessage: 'Email support@example.com.' }) + ).rejects.toThrow('helpMessage must contain'); + }); + + it('should throw if optInProofUrl is not http(s)', async () => { + await expect( + campaigns.create({ ...validCampaign, optInProofUrl: 'ftp://bad.com' }) + ).rejects.toThrow('optInProofUrl must start with'); + }); + + it('should throw if termsLink is not http(s)', async () => { + await expect( + campaigns.create({ ...validCampaign, termsLink: 'ftp://bad.com' }) + ).rejects.toThrow('termsLink must start with'); + }); + + it('should throw if privacyLink is not http(s)', async () => { + await expect( + campaigns.create({ ...validCampaign, privacyLink: 'ftp://bad.com' }) + ).rejects.toThrow('privacyLink must start with'); + }); + }); + + describe('get()', () => { + it('should get a campaign by ID', async () => { + const result = await campaigns.get(1); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'get', + '/v1/campaigns/1', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('list()', () => { + it('should list all campaigns', async () => { + mockCustomRequest.mockResolvedValue([mockResponse]); + const result = await campaigns.list(); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'get', + '/v1/campaigns', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + expect(result).toEqual([mockResponse]); + }); + }); + + describe('update()', () => { + it('should update a campaign with partial data', async () => { + const data = { description: 'Updated' }; + await campaigns.update(1, data); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'patch', + '/v1/campaigns/1', + data, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + }); + + it('should validate fields present in update', async () => { + await expect(campaigns.update(1, { useCase: 'INVALID' })).rejects.toThrow('Invalid use case'); + }); + }); + + describe('delete()', () => { + it('should delete a campaign', async () => { + await campaigns.delete(1); + expect(mockCustomRequest).toHaveBeenCalledWith( + 'delete', + '/v1/campaigns/1', + undefined, + 'https://compliance-test-cloudcontactai.allcode.com/api' + ); + }); + }); +}); diff --git a/src/brands/brands.ts b/src/brands/brands.ts new file mode 100644 index 0000000..65f07a5 --- /dev/null +++ b/src/brands/brands.ts @@ -0,0 +1,206 @@ +/** + * Brand service for managing brand registrations via CloudContactAI API + * + * @license MIT + * @copyright 2025 CloudContactAI LLC + */ + +import { CCAI } from '../ccai'; + +export type BrandData = { + legalCompanyName?: string; + dba?: string; + entityType?: string; + taxId?: string; + taxIdCountry?: string; + country?: string; + verticalType?: string; + websiteUrl?: string; + stockSymbol?: string; + stockExchange?: string; + street?: string; + city?: string; + state?: string; + postalCode?: string; + contactFirstName?: string; + contactLastName?: string; + contactEmail?: string; + contactPhone?: string; + websiteMatch?: boolean; +}; + +export type BrandResponse = BrandData & { + id: number; + accountId: number; + websiteMatchScore: number | null; + createdAt: string; + updatedAt: string; +}; + +const ENTITY_TYPES = new Set([ + 'PRIVATE_PROFIT', + 'PUBLIC_PROFIT', + 'NON_PROFIT', + 'GOVERNMENT', + 'SOLE_PROPRIETOR', +]); +const VERTICAL_TYPES = new Set([ + 'AUTOMOTIVE', + 'AGRICULTURE', + 'BANKING', + 'COMMUNICATION', + 'CONSTRUCTION', + 'EDUCATION', + 'ENERGY', + 'ENTERTAINMENT', + 'GOVERNMENT', + 'HEALTHCARE', + 'HOSPITALITY', + 'INSURANCE', + 'LEGAL', + 'MANUFACTURING', + 'NON_PROFIT', + 'PROFESSIONAL', + 'REAL_ESTATE', + 'RETAIL', + 'TECHNOLOGY', + 'TRANSPORTATION', +]); +const TAX_ID_COUNTRIES = new Set(['US', 'CA', 'GB', 'AU']); +const STOCK_EXCHANGES = new Set(['NASDAQ', 'NYSE', 'AMEX', 'TSX', 'LON', 'JPX', 'HKEX', 'OTHER']); + +function validateBrand(data: BrandData, isCreate: boolean): void { + const errors: { field: string; message: string }[] = []; + + if (isCreate) { + const required = [ + 'legalCompanyName', + 'entityType', + 'taxId', + 'taxIdCountry', + 'country', + 'verticalType', + 'websiteUrl', + 'street', + 'city', + 'state', + 'postalCode', + 'contactFirstName', + 'contactLastName', + 'contactEmail', + 'contactPhone', + ] as const; + for (const field of required) { + if (!data[field]) errors.push({ field, message: `${field} is required` }); + } + } + + if (data.entityType && !ENTITY_TYPES.has(data.entityType)) + errors.push({ field: 'entityType', message: 'Invalid entity type' }); + if (data.verticalType && !VERTICAL_TYPES.has(data.verticalType)) + errors.push({ field: 'verticalType', message: 'Invalid vertical type' }); + if (data.taxIdCountry && !TAX_ID_COUNTRIES.has(data.taxIdCountry)) + errors.push({ field: 'taxIdCountry', message: 'Invalid tax ID country' }); + if (data.stockExchange && !STOCK_EXCHANGES.has(data.stockExchange)) + errors.push({ field: 'stockExchange', message: 'Invalid stock exchange' }); + + if ( + data.websiteUrl && + !data.websiteUrl.startsWith('http://') && + !data.websiteUrl.startsWith('https://') + ) { + errors.push({ + field: 'websiteUrl', + message: 'Website URL must start with http:// or https://', + }); + } + + if (data.contactEmail && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(data.contactEmail)) { + errors.push({ field: 'contactEmail', message: 'Invalid email format' }); + } + + if ( + data.taxId && + data.taxIdCountry && + (data.taxIdCountry === 'US' || data.taxIdCountry === 'CA') + ) { + if (!/^\d{9}$/.test(data.taxId)) { + errors.push({ + field: 'taxId', + message: `Tax ID must be exactly 9 digits for ${data.taxIdCountry}`, + }); + } + } + + if (data.entityType === 'PUBLIC_PROFIT') { + if (!data.stockSymbol) + errors.push({ + field: 'stockSymbol', + message: 'Stock symbol is required for PUBLIC_PROFIT entities', + }); + if (!data.stockExchange) + errors.push({ + field: 'stockExchange', + message: 'Stock exchange is required for PUBLIC_PROFIT entities', + }); + } + + if (errors.length > 0) { + throw new Error(`Validation failed: ${JSON.stringify(errors)}`); + } +} + +export class Brands { + private ccai: CCAI; + + constructor(ccai: CCAI) { + this.ccai = ccai; + } + + async create(data: BrandData): Promise { + validateBrand(data, true); + return this.ccai.customRequest( + 'post', + '/v1/brands', + data, + this.ccai.getComplianceBaseUrl() + ); + } + + async get(id: number): Promise { + return this.ccai.customRequest( + 'get', + `/v1/brands/${id}`, + undefined, + this.ccai.getComplianceBaseUrl() + ); + } + + async list(): Promise { + return this.ccai.customRequest( + 'get', + '/v1/brands', + undefined, + this.ccai.getComplianceBaseUrl() + ); + } + + async update(id: number, data: BrandData): Promise { + validateBrand(data, false); + return this.ccai.customRequest( + 'patch', + `/v1/brands/${id}`, + data, + this.ccai.getComplianceBaseUrl() + ); + } + + async delete(id: number): Promise { + await this.ccai.customRequest( + 'delete', + `/v1/brands/${id}`, + undefined, + this.ccai.getComplianceBaseUrl() + ); + } +} diff --git a/src/campaigns/campaigns.ts b/src/campaigns/campaigns.ts new file mode 100644 index 0000000..e150aab --- /dev/null +++ b/src/campaigns/campaigns.ts @@ -0,0 +1,270 @@ +/** + * Campaign service for managing campaign registrations via CloudContactAI API + * + * @license MIT + * @copyright 2025 CloudContactAI LLC + */ + +import { CCAI } from '../ccai'; + +export type CampaignData = { + brandId?: number; + useCase?: string; + subUseCases?: string[]; + description?: string; + messageFlow?: string; + termsLink?: string; + privacyLink?: string; + hasEmbeddedLinks?: boolean; + hasEmbeddedPhone?: boolean; + isAgeGated?: boolean; + isDirectLending?: boolean; + optInKeywords?: string[]; + optInMessage?: string; + optInProofUrl?: string; + helpKeywords?: string[]; + helpMessage?: string; + optOutKeywords?: string[]; + optOutMessage?: string; + sampleMessages?: string[]; +}; + +export type CampaignResponse = CampaignData & { + id: number; + accountId: number; + monthlyFee: number; + createdAt: string; + updatedAt: string; +}; + +const CAMPAIGN_USE_CASES = new Set([ + 'TWO_FACTOR_AUTHENTICATION', + 'ACCOUNT_NOTIFICATION', + 'CUSTOMER_CARE', + 'DELIVERY_NOTIFICATION', + 'FRAUD_ALERT', + 'HIGHER_EDUCATION', + 'LOW_VOLUME_MIXED', + 'MARKETING', + 'MIXED', + 'POLLING_VOTING', + 'PUBLIC_SERVICE_ANNOUNCEMENT', + 'SECURITY_ALERT', +]); + +const CAMPAIGN_SUB_USE_CASES = new Set([ + 'TWO_FACTOR_AUTHENTICATION', + 'ACCOUNT_NOTIFICATION', + 'CUSTOMER_CARE', + 'DELIVERY_NOTIFICATION', + 'FRAUD_ALERT', + 'MARKETING', + 'POLLING_VOTING', +]); + +const MIXED_USE_CASES = new Set(['MIXED', 'LOW_VOLUME_MIXED']); + +const REQUIRED_FIELDS = [ + 'brandId', + 'useCase', + 'description', + 'messageFlow', + 'hasEmbeddedLinks', + 'hasEmbeddedPhone', + 'isAgeGated', + 'isDirectLending', + 'optInKeywords', + 'optInMessage', + 'optInProofUrl', + 'helpKeywords', + 'helpMessage', + 'optOutKeywords', + 'optOutMessage', + 'sampleMessages', +] as const; + +function validateCampaign(data: CampaignData, isCreate: boolean): void { + const errors: { field: string; message: string }[] = []; + + if (isCreate) { + for (const field of REQUIRED_FIELDS) { + const value = (data as Record)[field]; + if (value === undefined || value === null || value === '') { + errors.push({ field, message: `${field} is required` }); + } + } + } + + if (data.useCase && !CAMPAIGN_USE_CASES.has(data.useCase)) { + errors.push({ field: 'useCase', message: 'Invalid use case' }); + } + + const useCase = data.useCase; + const subUseCases = data.subUseCases; + + if (useCase && MIXED_USE_CASES.has(useCase)) { + if ( + !subUseCases || + !Array.isArray(subUseCases) || + subUseCases.length < 2 || + subUseCases.length > 3 + ) { + errors.push({ + field: 'subUseCases', + message: 'MIXED/LOW_VOLUME_MIXED requires 2-3 sub use cases', + }); + } else { + for (const suc of subUseCases) { + if (!CAMPAIGN_SUB_USE_CASES.has(suc)) { + errors.push({ field: 'subUseCases', message: `Invalid sub use case: ${suc}` }); + } + } + } + } else if (useCase && subUseCases) { + errors.push({ + field: 'subUseCases', + message: 'subUseCases should be empty for non-MIXED use cases', + }); + } + + if (data.sampleMessages !== undefined) { + if ( + !Array.isArray(data.sampleMessages) || + data.sampleMessages.length < 2 || + data.sampleMessages.length > 5 + ) { + errors.push({ field: 'sampleMessages', message: 'sampleMessages must contain 2-5 items' }); + } else { + const optOutKeywords = data.optOutKeywords || []; + const helpKeywords = data.helpKeywords || []; + + const hasOptOut = data.sampleMessages.some( + (msg) => + msg.includes('Reply STOP') || optOutKeywords.some((kw) => msg.includes(`Reply ${kw}`)) + ); + if (!hasOptOut) { + errors.push({ + field: 'sampleMessages', + message: "At least one sample must contain 'Reply STOP' or 'Reply {optOutKeyword}'", + }); + } + + const hasHelp = data.sampleMessages.some( + (msg) => + msg.includes('Reply HELP') || helpKeywords.some((kw) => msg.includes(`Reply ${kw}`)) + ); + if (!hasHelp) { + errors.push({ + field: 'sampleMessages', + message: "At least one sample must contain 'Reply HELP' or 'Reply {helpKeyword}'", + }); + } + } + } + + if (data.optOutMessage !== undefined) { + const optOutKeywords = data.optOutKeywords || []; + if ( + !data.optOutMessage.includes('STOP') && + !optOutKeywords.some((kw) => data.optOutMessage?.includes(kw)) + ) { + errors.push({ + field: 'optOutMessage', + message: "optOutMessage must contain 'STOP' or at least one optOutKeyword", + }); + } + } + + if (data.helpMessage !== undefined) { + const helpKeywords = data.helpKeywords || []; + if ( + !data.helpMessage.includes('HELP') && + !helpKeywords.some((kw) => data.helpMessage?.includes(kw)) + ) { + errors.push({ + field: 'helpMessage', + message: "helpMessage must contain 'HELP' or at least one helpKeyword", + }); + } + } + + if (data.optInProofUrl !== undefined) { + if (!data.optInProofUrl.startsWith('http://') && !data.optInProofUrl.startsWith('https://')) { + errors.push({ + field: 'optInProofUrl', + message: 'optInProofUrl must start with http:// or https://', + }); + } + } + + for (const linkField of ['termsLink', 'privacyLink'] as const) { + const val = data[linkField]; + if (val) { + if (!val.startsWith('http://') && !val.startsWith('https://')) { + errors.push({ + field: linkField, + message: `${linkField} must start with http:// or https://`, + }); + } + } + } + + if (errors.length > 0) { + throw new Error(`Validation failed: ${JSON.stringify(errors)}`); + } +} + +export class Campaigns { + private ccai: CCAI; + + constructor(ccai: CCAI) { + this.ccai = ccai; + } + + async create(data: CampaignData): Promise { + validateCampaign(data, true); + return this.ccai.customRequest( + 'post', + '/v1/campaigns', + data, + this.ccai.getComplianceBaseUrl() + ); + } + + async get(id: number): Promise { + return this.ccai.customRequest( + 'get', + `/v1/campaigns/${id}`, + undefined, + this.ccai.getComplianceBaseUrl() + ); + } + + async list(): Promise { + return this.ccai.customRequest( + 'get', + '/v1/campaigns', + undefined, + this.ccai.getComplianceBaseUrl() + ); + } + + async update(id: number, data: CampaignData): Promise { + validateCampaign(data, false); + return this.ccai.customRequest( + 'patch', + `/v1/campaigns/${id}`, + data, + this.ccai.getComplianceBaseUrl() + ); + } + + async delete(id: number): Promise { + await this.ccai.customRequest( + 'delete', + `/v1/campaigns/${id}`, + undefined, + this.ccai.getComplianceBaseUrl() + ); + } +} diff --git a/src/ccai.ts b/src/ccai.ts index dcc427b..f6eaf58 100644 --- a/src/ccai.ts +++ b/src/ccai.ts @@ -8,7 +8,10 @@ */ import axios, { AxiosResponse } from 'axios'; +import { Brands } from './brands/brands'; +import { Campaigns } from './campaigns/campaigns'; import { Contact } from './contact/contact'; +import { ContactValidator } from './contact-validator/contact-validator'; import { Email } from './email/email'; import { MMS } from './sms/mms'; import { SMS } from './sms/sms'; @@ -18,11 +21,13 @@ import { Webhook } from './webhook/webhook'; const PROD_BASE_URL = 'https://core.cloudcontactai.com/api'; const PROD_EMAIL_URL = 'https://email-campaigns.cloudcontactai.com/api/v1'; const PROD_FILES_URL = 'https://files.cloudcontactai.com'; +const PROD_COMPLIANCE_URL = 'https://compliance.cloudcontactai.com/api'; // Test environment URLs const TEST_BASE_URL = 'https://core-test-cloudcontactai.allcode.com/api'; const TEST_EMAIL_URL = 'https://email-campaigns-test-cloudcontactai.allcode.com/api/v1'; const TEST_FILES_URL = 'https://files-test-cloudcontactai.allcode.com'; +const TEST_COMPLIANCE_URL = 'https://compliance-test-cloudcontactai.allcode.com/api'; /** * Account representing a message recipient. @@ -73,6 +78,8 @@ export type CCAIConfig = { emailBaseUrl?: string; /** Override base URL for the Files API */ filesBaseUrl?: string; + /** Override base URL for the Compliance API */ + complianceBaseUrl?: string; }; /** @@ -84,6 +91,7 @@ export class CCAI { private baseUrl: string; private emailBaseUrl: string; private filesBaseUrl: string; + private complianceBaseUrl: string; private useTestEnvironment: boolean; /** SMS service for sending text messages */ @@ -97,10 +105,15 @@ export class CCAI { /** Webhook service for managing webhook endpoints */ public webhook: Webhook; + public brands: Brands; + public campaigns: Campaigns; /** Contact service for managing contact preferences */ public contact: Contact; + /** Contact validator service for validating emails and phone numbers */ + public contactValidator: ContactValidator; + /** * Create a new CCAI client instance * @param config - Configuration object @@ -135,12 +148,22 @@ export class CCAI { TEST_FILES_URL ); + this.complianceBaseUrl = this.resolveUrl( + config.complianceBaseUrl, + process.env.CCAI_COMPLIANCE_BASE_URL, + PROD_COMPLIANCE_URL, + TEST_COMPLIANCE_URL + ); + // Initialize the services this.sms = new SMS(this); this.mms = new MMS(this); this.email = new Email(this); this.webhook = new Webhook(this); this.contact = new Contact(this); + this.brands = new Brands(this); + this.campaigns = new Campaigns(this); + this.contactValidator = new ContactValidator(this); } /** @@ -197,6 +220,14 @@ export class CCAI { return this.filesBaseUrl; } + /** + * Get the base URL for the Compliance API + * @returns The compliance base URL + */ + getComplianceBaseUrl(): string { + return this.complianceBaseUrl; + } + /** * Whether the test environment is active * @returns True if using test environment diff --git a/src/contact-validator/contact-validator.ts b/src/contact-validator/contact-validator.ts new file mode 100644 index 0000000..8e6e130 --- /dev/null +++ b/src/contact-validator/contact-validator.ts @@ -0,0 +1,108 @@ +/** + * contact-validator.ts - A TypeScript module for validating email and phone contacts via CloudContactAI + * + * @license MIT + * @copyright 2025 CloudContactAI LLC + */ + +import { CCAI } from '../ccai'; + +export type ValidationStatus = 'valid' | 'invalid' | 'risky' | 'landline'; + +export type EmailValidationMetadata = { + safe_to_send?: boolean; + ai_verdict?: string | null; + [key: string]: unknown; +}; + +export type PhoneValidationMetadata = { + country_code?: string | null; + national_number?: string | null; + carrier_type?: string | null; + [key: string]: unknown; +}; + +export type EmailValidationResult = { + contact: string; + type: 'email'; + status: ValidationStatus; + metadata: EmailValidationMetadata; +}; + +export type PhoneValidationResult = { + contact: string; + type: 'phone'; + status: ValidationStatus; + metadata: PhoneValidationMetadata; +}; + +export type ValidationSummary = { + total: number; + valid: number; + invalid: number; + risky: number; + landline?: number; +}; + +export type BulkEmailValidationResult = { + results: EmailValidationResult[]; + summary: ValidationSummary; +}; + +export type BulkPhoneValidationResult = { + results: PhoneValidationResult[]; + summary: ValidationSummary; +}; + +export type PhoneInput = { + phone: string; + countryCode?: string; +}; + +/** + * Service for validating email addresses and phone numbers + */ +export class ContactValidator { + private ccai: CCAI; + + constructor(ccai: CCAI) { + this.ccai = ccai; + } + + /** + * Validate a single email address + * @param email - Email address to validate + * @returns Promise resolving to the validation result + */ + validateEmail(email: string): Promise { + return this.ccai.request('POST', '/v1/contact-validator/email', { email }); + } + + /** + * Validate multiple email addresses (up to 50) + * @param emails - List of email addresses to validate + * @returns Promise resolving to bulk validation results with summary + */ + validateEmails(emails: string[]): Promise { + return this.ccai.request('POST', '/v1/contact-validator/emails', { emails }); + } + + /** + * Validate a single phone number + * @param phone - Phone number in E.164 format (e.g. +15551234567) + * @param countryCode - Optional ISO 3166-1 alpha-2 country code (e.g. "US") + * @returns Promise resolving to the validation result + */ + validatePhone(phone: string, countryCode?: string): Promise { + return this.ccai.request('POST', '/v1/contact-validator/phone', { phone, countryCode }); + } + + /** + * Validate multiple phone numbers (up to 50) + * @param phones - List of phone inputs with optional country codes + * @returns Promise resolving to bulk validation results with summary + */ + validatePhones(phones: PhoneInput[]): Promise { + return this.ccai.request('POST', '/v1/contact-validator/phones', { phones }); + } +} diff --git a/src/examples/brands-example.ts b/src/examples/brands-example.ts new file mode 100644 index 0000000..7414e22 --- /dev/null +++ b/src/examples/brands-example.ts @@ -0,0 +1,76 @@ +/** + * Example usage of the CCAI Brands API + * + * @license MIT + * @copyright 2025 CloudContactAI LLC + */ + +import dotenv from 'dotenv'; +import { CCAI } from '../ccai'; + +// Load environment variables +dotenv.config(); + +// Create a new CCAI client +const ccai = new CCAI({ + clientId: process.env.CCAI_CLIENT_ID || '', + apiKey: process.env.CCAI_API_KEY || '', +}); + +async function brandExamples() { + try { + // Create a brand + console.log('Creating a brand...'); + const brand = await ccai.brands.create({ + legalCompanyName: 'Collect.org Inc.', + dba: 'Collect', + entityType: 'NON_PROFIT', + taxId: '123456789', + taxIdCountry: 'US', + country: 'US', + verticalType: 'NON_PROFIT', + websiteUrl: 'https://www.collect.org', + street: '123 Main Street', + city: 'San Francisco', + state: 'CA', + postalCode: '94105', + contactFirstName: 'Jane', + contactLastName: 'Doe', + contactEmail: 'jane@collect.org', + contactPhone: '+14155551234', + }); + console.log('Brand created:', brand); + + // Get brand by ID + console.log('\nFetching brand by ID...'); + const fetched = await ccai.brands.get(brand.id); + console.log('Brand details:', fetched); + + // List all brands + console.log('\nListing all brands...'); + const brands = await ccai.brands.list(); + console.log(`Found ${brands.length} brand(s)`); + + // Update a brand + console.log('\nUpdating brand...'); + const updated = await ccai.brands.update(brand.id, { + street: '456 Oak Avenue', + city: 'Los Angeles', + contactEmail: 'admin@collect.org', + }); + console.log('Brand updated:', updated); + + // Delete a brand + console.log('\nDeleting brand...'); + await ccai.brands.delete(brand.id); + console.log('Brand deleted successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Error:', error.message); + } else { + console.error('Error:', error); + } + } +} + +brandExamples(); diff --git a/src/examples/campaigns-example.ts b/src/examples/campaigns-example.ts new file mode 100644 index 0000000..aa74418 --- /dev/null +++ b/src/examples/campaigns-example.ts @@ -0,0 +1,83 @@ +/** + * Example usage of the CCAI Campaigns API + * + * @license MIT + * @copyright 2025 CloudContactAI LLC + */ + +import dotenv from 'dotenv'; +import { CCAI } from '../ccai'; + +dotenv.config(); + +const ccai = new CCAI({ + clientId: process.env.CCAI_CLIENT_ID || '', + apiKey: process.env.CCAI_API_KEY || '', +}); + +async function campaignExamples() { + try { + // Create a campaign (assumes brand ID 1 exists) + console.log('Creating a campaign...'); + const campaign = await ccai.campaigns.create({ + brandId: 1, + useCase: 'MIXED', + subUseCases: ['CUSTOMER_CARE', 'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION'], + description: 'This campaign handles security codes and support for Collect.org.', + messageFlow: 'Users opt-in via our signup form checkbox at https://collect.org/signup', + termsLink: 'https://collect.org/terms', + privacyLink: 'https://collect.org/privacy', + hasEmbeddedLinks: true, + hasEmbeddedPhone: false, + isAgeGated: false, + isDirectLending: false, + optInKeywords: ['START', 'JOIN'], + optInMessage: 'Welcome to Collect.org! Msg&Data rates may apply. Reply STOP to cancel.', + optInProofUrl: 'https://collect.org/images/opt-in-proof.png', + helpKeywords: ['HELP', 'INFO'], + helpMessage: 'Collect.org: For help email support@collect.org. Reply STOP to cancel.', + optOutKeywords: ['STOP', 'UNSUBSCRIBE'], + optOutMessage: 'Collect.org: You have been unsubscribed. STOP received.', + sampleMessages: [ + 'Your Collect.org security code is 554321. Reply STOP to cancel.', + 'Hi [Name], your ticket #[ID] has been updated. Reply HELP for more info.', + ], + }); + console.log('Campaign created:', campaign.id, `fee: $${campaign.monthlyFee}/mo`); + + // Get campaign by ID + console.log('\nFetching campaign by ID...'); + const fetched = await ccai.campaigns.get(campaign.id); + console.log('Campaign:', fetched.useCase, 'Brand:', fetched.brandId); + + // List all campaigns + console.log('\nListing all campaigns...'); + const campaigns = await ccai.campaigns.list(); + console.log(`Found ${campaigns.length} campaign(s)`); + + // Update a campaign + console.log('\nUpdating campaign...'); + const updated = await ccai.campaigns.update(campaign.id, { + description: 'Updated campaign description for Collect.org messaging.', + sampleMessages: [ + 'Your Collect.org code is 123456. Reply STOP to opt-out.', + 'Your support ticket has been resolved. Reply HELP for more info.', + 'Your payment of $50.00 was received. Reply STOP to cancel.', + ], + }); + console.log('Campaign updated:', updated.description); + + // Delete a campaign + console.log('\nDeleting campaign...'); + await ccai.campaigns.delete(campaign.id); + console.log('Campaign deleted successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Error:', error.message); + } else { + console.error('Error:', error); + } + } +} + +campaignExamples(); diff --git a/src/index.ts b/src/index.ts index f536ec9..9c4d95d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,26 @@ * @copyright 2025 CloudContactAI LLC */ +import { Brands } from './brands/brands'; +import type { BrandData, BrandResponse } from './brands/brands'; +import { Campaigns } from './campaigns/campaigns'; +import type { CampaignData, CampaignResponse } from './campaigns/campaigns'; import { CCAI } from './ccai'; import type { Account, CCAIConfig } from './ccai'; import { Contact } from './contact/contact'; import type { SetDoNotTextResponse } from './contact/contact'; +import { ContactValidator } from './contact-validator/contact-validator'; +import type { + BulkEmailValidationResult, + BulkPhoneValidationResult, + EmailValidationMetadata, + EmailValidationResult, + PhoneInput, + PhoneValidationMetadata, + PhoneValidationResult, + ValidationStatus, + ValidationSummary, +} from './contact-validator/contact-validator'; import { Email } from './email/email'; import type { EmailAccount, EmailCampaign, EmailOptions, EmailResponse } from './email/email'; import { MMS } from './sms/mms'; @@ -22,7 +38,19 @@ import { WebhookEventType } from './webhook/types'; import { Webhook } from './webhook/webhook'; // Re-export classes -export { CCAI, SMS, MMS, Email, Webhook, WebhookEventType, createWebhookHandler, Contact }; +export { + CCAI, + SMS, + MMS, + Email, + Webhook, + WebhookEventType, + createWebhookHandler, + Contact, + ContactValidator, + Brands, + Campaigns, +}; // Re-export types using 'export type' export type { @@ -40,4 +68,17 @@ export type { WebhookEvent, WebhookHandlerOptions, SetDoNotTextResponse, + BrandData, + BrandResponse, + CampaignData, + CampaignResponse, + ValidationStatus, + ValidationSummary, + EmailValidationResult, + EmailValidationMetadata, + PhoneValidationResult, + PhoneValidationMetadata, + BulkEmailValidationResult, + BulkPhoneValidationResult, + PhoneInput, };