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
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,130 @@ async function sendMmsWithImage() {
sendMmsWithImage();
```

### 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);

// 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
Expand Down Expand Up @@ -542,6 +666,8 @@ 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)
- Webhook management: register, list, update, delete
- Webhook signature verification (HMAC-SHA256)
Expand Down
187 changes: 187 additions & 0 deletions src/__tests__/brands.test.ts
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
});
Loading
Loading