diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..b1b1807
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+.git
+.github
+*.md
+LICENSE
+example/
+target/
+.env
+.env.*
+*.log
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..00768bb
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,58 @@
+name: Publish to Maven Central
+
+on:
+ push:
+ tags:
+ - 'release/v*'
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Import GPG key
+ run: echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
+ env:
+ GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+
+ - name: Configure Maven settings
+ run: |
+ mkdir -p ~/.m2
+ cat > ~/.m2/settings.xml << 'EOF'
+
+
+
+ central
+ ${env.MAVEN_CENTRAL_USERNAME}
+ ${env.MAVEN_CENTRAL_TOKEN}
+
+
+
+
+ gpg
+
+ ${env.GPG_PASSPHRASE}
+
+
+
+
+ gpg
+
+
+ EOF
+
+ - name: Build and publish
+ run: ./mvnw deploy -DskipTests
+ env:
+ MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
+ MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.gitignore b/.gitignore
index 098621a..d3ea1f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,3 +97,6 @@ yarn-error.log*
# Test files
test-image.jpg
+
+# Claude
+.claude/
diff --git a/README.md b/README.md
index b1cd3fe..d94d6af 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,11 @@ A Kotlin/Java client library for interacting with the [CloudContactAI](https://c
- Send SMS messages to single or multiple recipients
- Send MMS messages with images to single or multiple recipients
- Send Email campaigns to single or multiple recipients
+- Brand registration and management for TCR verification
+- Campaign registration and management for TCR carrier vetting
- Manage webhooks for event notifications
- Webhook signature validation for security
+- Validate email addresses (valid/invalid/risky) and phone numbers (valid/invalid/landline)
- Variable substitution in messages
- Test environment support
- Comprehensive error handling
@@ -29,7 +32,7 @@ Add this dependency to your `pom.xml`:
com.cloudcontactai
ccai-java-sdk
- 1.0.5
+ 1.0.9
```
@@ -38,7 +41,7 @@ Add this dependency to your `pom.xml`:
Add this dependency to your `build.gradle`:
```gradle
-implementation 'com.cloudcontactai:ccai-java-sdk:1.0.5'
+implementation 'com.cloudcontactai:ccai-java-sdk:1.0.9'
```
## Configuration
@@ -245,15 +248,12 @@ For optimal MMS delivery and performance:
```kotlin
import com.cloudcontactai.sdk.mms.Account
+import com.cloudcontactai.sdk.mms.SignedUploadUrlRequest
import java.io.File
-// Send MMS with automatic image upload (recommended)
+// ── Option A: All-in-one (recommended) ─────────────────────────────────────
val mmsAccounts = listOf(
- Account(
- firstName = "John",
- lastName = "Doe",
- phone = "+15551234567"
- )
+ Account(firstName = "John", lastName = "Doe", phone = "+15551234567")
)
val imageFile = File("path/to/image.jpg")
@@ -262,17 +262,82 @@ val mmsResponse = ccai.mms.sendWithImage(
message = "Check out this image!",
title = "MMS Campaign",
imageFile = imageFile
+ // optional: senderPhone = "+15559990000"
)
-
-// Response ID may be in campaignId or id field
val responseId = mmsResponse.campaignId ?: mmsResponse.id
println("MMS sent with ID: ${responseId}")
+
+// ── Option B: Manual workflow (step-by-step) ────────────────────────────────
+
+// Step 1 — Get a pre-signed S3 upload URL
+val uploadRequest = SignedUploadUrlRequest(fileName = "image.jpg", fileType = "image/jpeg")
+val uploadResponse = ccai.mms.getSignedUploadUrl(uploadRequest)
+
+// Step 2 — Upload the image to S3
+ccai.mms.uploadImageToSignedUrl(uploadResponse.signedS3Url, imageFile, "image/jpeg")
+
+// Step 3 — (Optional) Confirm the file is available
+val stored = ccai.mms.checkFileUploaded(uploadResponse.fileKey!!)
+println("File URL: ${stored.storedUrl}")
+
+// Step 4a — Send to multiple recipients using the uploaded fileKey
+val bulkResponse = ccai.mms.send(
+ accounts = mmsAccounts,
+ message = "Hello ${firstName}!",
+ title = "MMS Campaign",
+ pictureFileKey = uploadResponse.fileKey!!
+ // optional: senderPhone = "+15559990000"
+)
+
+// Step 4b — Send to a single recipient
+val singleResponse = ccai.mms.sendSingle(
+ firstName = "John",
+ lastName = "Doe",
+ phone = "+15551234567",
+ message = "Hello ${firstName}!",
+ title = "MMS Campaign",
+ pictureFileKey = uploadResponse.fileKey!!
+ // optional: senderPhone = "+15559990000"
+)
+```
+
+#### Contact Validator
+
+Validate email addresses and phone numbers.
+
+```kotlin
+import com.cloudcontactai.sdk.contactvalidator.PhoneInput
+
+// Validate a single email
+val emailResult = ccai.contactValidator.validateEmail("user@example.com")
+println(emailResult.status) // "valid" | "invalid" | "risky"
+println(emailResult.metadata["safe_to_send"]) // true | false
+
+// Validate multiple emails (up to 50)
+val bulkEmails = ccai.contactValidator.validateEmails(listOf(
+ "user@example.com",
+ "bad@invalid.xyz"
+))
+println(bulkEmails.summary) // ValidationSummary(total=2, valid=1, invalid=1, risky=0, landline=0)
+
+// Validate a single phone number
+val phoneResult = ccai.contactValidator.validatePhone("+15551234567", countryCode = "US")
+println(phoneResult.status) // "valid" | "invalid" | "landline"
+println(phoneResult.metadata["carrier_type"]) // "mobile" | "landline" | "voip"
+
+// Validate multiple phone numbers (up to 50)
+val bulkPhones = ccai.contactValidator.validatePhones(listOf(
+ PhoneInput(phone = "+15551234567"),
+ PhoneInput(phone = "+15559876543", countryCode = "US")
+))
+println(bulkPhones.summary) // ValidationSummary(total=2, valid=1, invalid=0, risky=0, landline=1)
```
#### Webhook Management
```kotlin
import com.cloudcontactai.sdk.webhook.WebhookRequest
+import com.cloudcontactai.sdk.webhook.WebhookUpdateRequest
// Create a webhook (auto-generated secret)
val webhook = ccai.webhook.create(WebhookRequest("https://your-app.com/webhooks/ccai"))
@@ -286,20 +351,27 @@ val customWebhook = ccai.webhook.create(
)
println("Webhook created with custom secret!")
-// Get the webhook
-val webhookDetails = ccai.webhook.get()
-webhookDetails?.let {
- println("Current webhook URL: ${it.url}")
- println("Method: ${it.method}")
- println("Secret Key: ${it.secretKey}")
+// Get all webhooks
+val allWebhooks = ccai.webhook.getAll()
+allWebhooks.forEach { wh ->
+ println("Webhook ID: ${wh.id}, URL: ${wh.url}")
}
+// Get a specific webhook by ID
+val webhookDetails = ccai.webhook.get(webhook.id)
+println("Current webhook URL: ${webhookDetails.url}")
+println("Method: ${webhookDetails.method}")
+println("Secret Key: ${webhookDetails.secretKey}")
+
// Update webhook
val updated = ccai.webhook.update(
- WebhookRequest("https://your-app.com/webhooks/ccai-updated", "my-custom-secret-32chars12345")
+ WebhookUpdateRequest(webhook.id, "https://your-app.com/webhooks/ccai-updated", "my-custom-secret-32chars12345")
)
println("Webhook updated to: ${updated.url}")
+// Delete a webhook
+ccai.webhook.delete(webhook.id)
+
// Validate CloudContactAI webhook signature (using eventHash)
val payload = """
{
@@ -330,6 +402,123 @@ if (isValid) {
}
```
+#### Brand Registration
+
+```kotlin
+import com.cloudcontactai.sdk.brands.BrandRequest
+
+// Create a brand
+val brand = ccai.brands.create(BrandRequest(
+ 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"
+))
+println("Brand created with ID: ${brand.id}")
+
+// Get a brand by ID
+val fetched = ccai.brands.get(brand.id)
+println("Website match score: ${fetched.websiteMatchScore ?: "pending"}")
+
+// List all brands for the account
+val brands = ccai.brands.list()
+println("Found ${brands.size} brand(s)")
+
+// Update a brand (partial update)
+val updated = ccai.brands.update(brand.id, BrandRequest(
+ street = "456 Oak Avenue",
+ city = "Los Angeles"
+))
+
+// Delete a brand
+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`
+
+#### Campaign Registration
+
+```kotlin
+import com.cloudcontactai.sdk.campaigns.CampaignRequest
+
+// Create a campaign
+val campaign = ccai.campaigns.create(CampaignRequest(
+ brandId = 1,
+ useCase = "MIXED",
+ subUseCases = listOf("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 = listOf("START"),
+ optInMessage = "Welcome! Reply STOP to cancel.",
+ optInProofUrl = "https://example.com/opt-in-proof.png",
+ helpKeywords = listOf("HELP"),
+ helpMessage = "For HELP email support@example.com.",
+ optOutKeywords = listOf("STOP"),
+ optOutMessage = "STOP received. You are unsubscribed.",
+ sampleMessages = listOf(
+ "Your code is 554321. Reply STOP to cancel.",
+ "Your ticket has been updated. Reply HELP for info."
+ )
+))
+println("Campaign created with ID: ${campaign.id}")
+
+// Get a campaign by ID
+val fetched = ccai.campaigns.get(campaign.id)
+
+// List all campaigns for the account
+val campaigns = ccai.campaigns.list()
+println("Found ${campaigns.size} campaign(s)")
+
+// Update a campaign (partial update)
+val updated = ccai.campaigns.update(campaign.id, CampaignRequest(
+ description = "Updated description."
+))
+
+// Delete a campaign
+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`
+
+#### Contact Management
+
+```kotlin
+import com.cloudcontactai.sdk.contact.ContactService
+
+// Opt a contact out of text messages (by phone number)
+ccai.contact.setDoNotText(phone = "+15551234567", doNotText = true)
+
+// Opt a contact back in (by phone number)
+ccai.contact.setDoNotText(phone = "+15551234567", doNotText = false)
+
+// Opt out by contactId
+ccai.contact.setDoNotText(contactId = "contact-abc-123", doNotText = true)
+```
+
### Java Usage
```java
diff --git a/create-central-bundle.sh b/create-central-bundle.sh
index ca24cce..6a8faad 100755
--- a/create-central-bundle.sh
+++ b/create-central-bundle.sh
@@ -7,23 +7,23 @@ echo "Building Maven project..."
echo "Creating deployment structure..."
rm -rf central-bundle
-mkdir -p central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5
+mkdir -p central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9
# Copy artifacts
-cp target/ccai-java-sdk-1.0.5.jar central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5/
-cp target/ccai-java-sdk-1.0.5-sources.jar central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5/
-cp pom.xml central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5/ccai-java-sdk-1.0.5.pom
+cp target/ccai-java-sdk-1.0.9.jar central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9/
+cp target/ccai-java-sdk-1.0.9-sources.jar central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9/
+cp pom.xml central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9/ccai-java-sdk-1.0.9.pom
# Create javadoc jar
mkdir -p temp-javadoc
echo "# CCAI Java SDK Documentation" > temp-javadoc/README.md
echo "Visit https://github.com/cloudcontactai/ccai-java-sdk for documentation" >> temp-javadoc/README.md
cd temp-javadoc
-jar cf ../central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5/ccai-java-sdk-1.0.5-javadoc.jar *
+jar cf ../central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9/ccai-java-sdk-1.0.9-javadoc.jar *
cd ..
rm -rf temp-javadoc
-cd central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.5
+cd central-bundle/com/cloudcontactai/ccai-java-sdk/1.0.9
echo "Generating checksums and signatures..."
for file in *.jar *.pom; do
@@ -41,12 +41,12 @@ cd ../../../../..
echo "Creating ZIP archive..."
cd central-bundle
-zip -r ../ccai-java-sdk-1.0.5-central-bundle.zip com/
+zip -r ../ccai-java-sdk-1.0.9-central-bundle.zip com/
cd ..
-echo "Bundle created: ccai-java-sdk-1.0.5-central-bundle.zip"
+echo "Bundle created: ccai-java-sdk-1.0.9-central-bundle.zip"
echo "Upload this file to Maven Central Publisher Portal"
# Show structure
echo -e "\nBundle structure:"
-unzip -l ccai-java-sdk-1.0.5-central-bundle.zip
+unzip -l ccai-java-sdk-1.0.9-central-bundle.zip
diff --git a/example/src/main/java/com/example/javasdk/JavaSdkTestApplication.java b/example/src/main/java/com/example/javasdk/JavaSdkTestApplication.java
index 4de7725..0e293fe 100644
--- a/example/src/main/java/com/example/javasdk/JavaSdkTestApplication.java
+++ b/example/src/main/java/com/example/javasdk/JavaSdkTestApplication.java
@@ -2,6 +2,10 @@
import com.cloudcontactai.sdk.CCAIClient;
import com.cloudcontactai.sdk.common.CCAIConfig;
+import com.cloudcontactai.sdk.brands.BrandRequest;
+import com.cloudcontactai.sdk.brands.BrandResponse;
+import com.cloudcontactai.sdk.campaigns.CampaignRequest;
+import com.cloudcontactai.sdk.campaigns.CampaignResponse;
import com.cloudcontactai.sdk.contact.ContactDoNotTextResponse;
import com.cloudcontactai.sdk.sms.SMSResponse;
import com.cloudcontactai.sdk.mms.MMSResponse;
@@ -60,6 +64,12 @@ public static void main(String[] args) {
} else if("contact".equals(args[0])){
runDoNotTextContactTest(client);
return;
+ } else if("brands".equals(args[0])){
+ runBrandsTest(client);
+ return;
+ } else if("campaigns".equals(args[0])){
+ runCampaignsTest(client);
+ return;
}
}
SpringApplication.run(JavaSdkTestApplication.class, args);
@@ -226,4 +236,105 @@ private static void runDoNotTextContactTest(CCAIClient client) {
System.out.printf("Do not text Test Result: FAIL %s", e.getMessage());
}
}
+
+ private static void runBrandsTest(CCAIClient client) {
+ try {
+ System.out.println("Testing Brands CCAI Java SDK...");
+
+ // Create a brand
+ BrandRequest request = new BrandRequest(
+ "Collect.org Inc.", "Collect", "NON_PROFIT",
+ "123456789", "US", "US", "NON_PROFIT",
+ "https://www.collect.org", null, null,
+ "123 Main Street", "San Francisco", "CA", "94105",
+ "Jane", "Doe", "jane@collect.org", "+14155551234", false
+ );
+ BrandResponse brand = client.getBrands().create(request);
+ System.out.println("Brand created with ID: " + brand.getId());
+
+ // Get brand by ID
+ BrandResponse fetched = client.getBrands().get(brand.getId());
+ System.out.println("Brand: " + fetched.getLegalCompanyName() +
+ ", Score: " + fetched.getWebsiteMatchScore());
+
+ // List all brands
+ BrandResponse[] brands = client.getBrands().list();
+ System.out.println("Found " + brands.length + " brand(s)");
+
+ // Update a brand
+ BrandRequest updateRequest = new BrandRequest(
+ null, null, null, null, null, null, null, null, null, null,
+ "456 Oak Avenue", "Los Angeles", null, null,
+ null, null, "admin@collect.org", null, false
+ );
+ BrandResponse updated = client.getBrands().update(brand.getId(), updateRequest);
+ System.out.println("Brand updated: " + updated.getStreet() + ", " + updated.getCity());
+
+ // Delete a brand
+ client.getBrands().delete(brand.getId());
+ System.out.println("Brand deleted successfully");
+ } catch (Exception e) {
+ System.out.printf("Brands Test Result: FAIL %s%n", e.getMessage());
+ }
+ }
+
+ private static void runCampaignsTest(CCAIClient client) {
+ try {
+ System.out.println("Testing Campaigns CCAI Java SDK...");
+
+ // Create a campaign (assumes brand ID 1 exists)
+ CampaignResponse campaign = client.getCampaigns().create(new CampaignRequest(
+ 1L,
+ "MIXED",
+ Arrays.asList("CUSTOMER_CARE", "TWO_FACTOR_AUTHENTICATION", "ACCOUNT_NOTIFICATION"),
+ "This campaign handles security codes and support for Collect.org.",
+ "Users opt-in via our signup form checkbox at https://collect.org/signup",
+ "https://collect.org/terms",
+ "https://collect.org/privacy",
+ true,
+ false,
+ false,
+ false,
+ Arrays.asList("START", "JOIN"),
+ "Welcome to Collect.org! Msg&Data rates may apply. Reply STOP to cancel.",
+ "https://collect.org/images/opt-in-proof.png",
+ Arrays.asList("HELP", "INFO"),
+ "Collect.org: For help email support@collect.org. Reply STOP to cancel.",
+ Arrays.asList("STOP", "UNSUBSCRIBE"),
+ "Collect.org: You have been unsubscribed. STOP received.",
+ Arrays.asList(
+ "Your Collect.org security code is 554321. Reply STOP to cancel.",
+ "Hi [Name], your ticket #[ID] has been updated. Reply HELP for more info."
+ )
+ ));
+ System.out.println("Campaign created with ID: " + campaign.getId());
+
+ // Get campaign by ID
+ CampaignResponse fetched = client.getCampaigns().get(campaign.getId());
+ System.out.println("Campaign: " + fetched.getUseCase() + ", Brand: " + fetched.getBrandId());
+
+ // List all campaigns
+ CampaignResponse[] campaigns = client.getCampaigns().list();
+ System.out.println("Found " + campaigns.length + " campaign(s)");
+
+ // Update a campaign
+ CampaignResponse updated = client.getCampaigns().update(campaign.getId(), new CampaignRequest(
+ null, null, null,
+ "Updated campaign description for Collect.org messaging.",
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ Arrays.asList(
+ "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."
+ )
+ ));
+ System.out.println("Campaign updated: " + updated.getDescription());
+
+ // Delete a campaign
+ client.getCampaigns().delete(campaign.getId());
+ System.out.println("Campaign deleted successfully");
+ } catch (Exception e) {
+ System.out.printf("Campaigns Test Result: FAIL %s%n", e.getMessage());
+ }
+ }
}
diff --git a/integration/Dockerfile b/integration/Dockerfile
new file mode 100644
index 0000000..30e7f28
--- /dev/null
+++ b/integration/Dockerfile
@@ -0,0 +1,20 @@
+FROM maven:3.9-eclipse-temurin-17 AS build
+
+WORKDIR /sdk
+COPY pom.xml ./
+RUN mvn dependency:go-offline -q || true
+
+COPY src/ ./src/
+RUN mvn install -DskipTests -q
+
+WORKDIR /sdk/integration
+COPY integration/pom.xml ./
+RUN mvn dependency:go-offline -q || true
+
+COPY integration/ ./
+RUN mvn package -DskipTests -q
+
+FROM eclipse-temurin:17-jre-alpine
+WORKDIR /app
+COPY --from=build /sdk/integration/target/ccai-integration-test-java-1.0-shaded.jar ./test.jar
+CMD ["java", "-jar", "test.jar"]
diff --git a/integration/Dockerfile.release b/integration/Dockerfile.release
new file mode 100644
index 0000000..c96b753
--- /dev/null
+++ b/integration/Dockerfile.release
@@ -0,0 +1,17 @@
+FROM maven:3.9-eclipse-temurin-17 AS build
+
+ARG SDK_VERSION=1.0.9
+
+WORKDIR /sdk/integration
+COPY integration/pom.xml ./
+
+# Resolve the published SDK version from Maven Central (no local install step)
+RUN mvn dependency:go-offline -q -Dccai.sdk.version=${SDK_VERSION} || true
+
+COPY integration/ ./
+RUN mvn package -DskipTests -q -Dccai.sdk.version=${SDK_VERSION}
+
+FROM eclipse-temurin:17-jre-alpine
+WORKDIR /app
+COPY --from=build /sdk/integration/target/ccai-integration-test-java-1.0-shaded.jar ./test.jar
+CMD ["java", "-jar", "test.jar"]
diff --git a/integration/pom.xml b/integration/pom.xml
new file mode 100644
index 0000000..07c31c1
--- /dev/null
+++ b/integration/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ com.ccai
+ ccai-integration-test-java
+ 1.0
+ jar
+
+
+ 1.9.0
+ 11
+ 11
+ UTF-8
+
+ 1.0.5
+
+
+
+
+ com.cloudcontactai
+ ccai-java-sdk
+ ${ccai.sdk.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+ shade
+
+ ${project.build.directory}/ccai-integration-test-java-1.0-shaded.jar
+
+
+ com.ccai.integration.TestMainKt
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
diff --git a/integration/src/main/kotlin/com/ccai/integration/TestMain.kt b/integration/src/main/kotlin/com/ccai/integration/TestMain.kt
new file mode 100644
index 0000000..e398ab6
--- /dev/null
+++ b/integration/src/main/kotlin/com/ccai/integration/TestMain.kt
@@ -0,0 +1,520 @@
+package com.ccai.integration
+
+import com.cloudcontactai.sdk.CCAIClient
+import com.cloudcontactai.sdk.common.CCAIConfig
+import com.cloudcontactai.sdk.sms.Account as SmsAccount
+import com.cloudcontactai.sdk.mms.Account as MmsAccount
+import com.cloudcontactai.sdk.email.EmailAccount
+import com.cloudcontactai.sdk.webhook.WebhookRequest
+import com.cloudcontactai.sdk.webhook.WebhookUpdateRequest
+import com.cloudcontactai.sdk.brands.BrandRequest
+import com.cloudcontactai.sdk.campaigns.CampaignRequest
+import com.cloudcontactai.sdk.contactvalidator.PhoneInput
+import java.io.File
+import java.util.Base64
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import kotlin.system.exitProcess
+
+// ---------------------------------------------------------------------------
+// Environment variables
+// ---------------------------------------------------------------------------
+val clientId = System.getenv("CCAI_CLIENT_ID") ?: ""
+val apiKey = System.getenv("CCAI_API_KEY") ?: ""
+val phone1 = System.getenv("CCAI_TEST_PHONE") ?: ""
+val phone2 = System.getenv("CCAI_TEST_PHONE_2") ?: ""
+val phone3 = System.getenv("CCAI_TEST_PHONE_3") ?: ""
+val email1 = System.getenv("CCAI_TEST_EMAIL") ?: ""
+val email2 = System.getenv("CCAI_TEST_EMAIL_2") ?: ""
+val email3 = System.getenv("CCAI_TEST_EMAIL_3") ?: ""
+val first1 = System.getenv("CCAI_TEST_FIRST_NAME") ?: "Docker"
+val last1 = System.getenv("CCAI_TEST_LAST_NAME") ?: "Test"
+val first2 = System.getenv("CCAI_TEST_FIRST_NAME_2") ?: "Docker2"
+val last2 = System.getenv("CCAI_TEST_LAST_NAME_2") ?: "Test2"
+val first3 = System.getenv("CCAI_TEST_FIRST_NAME_3") ?: "Docker3"
+val last3 = System.getenv("CCAI_TEST_LAST_NAME_3") ?: "Test3"
+val webhookUrl = System.getenv("WEBHOOK_URL") ?: "https://webhook.site/java-docker-test"
+
+// ---------------------------------------------------------------------------
+// Test runner state
+// ---------------------------------------------------------------------------
+var passed = 0
+var failed = 0
+
+fun runTest(label: String, block: () -> Unit) {
+ try {
+ block()
+ println(" [PASS] $label")
+ passed++
+ } catch (e: Throwable) {
+ val msg = e.message?.lines()?.firstOrNull() ?: "unknown error"
+ println(" [FAIL] $label: $msg")
+ failed++
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+fun main() {
+ if (clientId.isBlank() || apiKey.isBlank()) {
+ System.err.println("ERROR: CCAI_CLIENT_ID and CCAI_API_KEY environment variables are required.")
+ exitProcess(1)
+ }
+
+ // Use CCAI_BASE_URL if set (local dev), otherwise fall back to test environment
+ val config = CCAIConfig(
+ clientId = clientId,
+ apiKey = apiKey,
+ useTestEnvironment = System.getenv("CCAI_BASE_URL") == null
+ )
+ val client = CCAIClient(config)
+
+ // Write a 1x1 transparent PNG to a temp file for MMS tests
+ val imageBytes = Base64.getDecoder().decode(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="
+ )
+ val imageFile = File.createTempFile("ccai_test", ".png").also {
+ it.writeBytes(imageBytes)
+ it.deleteOnExit()
+ }
+
+ println("=== CCAI Java (Kotlin) SDK Integration Tests ===\n")
+
+ // -----------------------------------------------------------------------
+ // SMS Tests (01–06)
+ // -----------------------------------------------------------------------
+ println("--- SMS ---")
+
+ runTest("01 SMS sendSingle") {
+ val res = client.sms.sendSingle(first1, last1, phone1, "Hello \${firstName}!", "Java Test 01")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("02 SMS send (1 recipient)") {
+ val accounts = listOf(SmsAccount(first1, last1, phone1))
+ val res = client.sms.send(accounts, "Bulk test \${firstName}", "Java Test 02")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("03 SMS send (2 recipients)") {
+ val accounts = listOf(
+ SmsAccount(first1, last1, phone1),
+ SmsAccount(first2, last2, phone2)
+ )
+ val res = client.sms.send(accounts, "Multi-recipient \${firstName}", "Java Test 03")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("04 SMS send (3 recipients)") {
+ val accounts = listOf(
+ SmsAccount(first1, last1, phone1),
+ SmsAccount(first2, last2, phone2),
+ SmsAccount(first3, last3, phone3)
+ )
+ val res = client.sms.send(accounts, "Triple-recipient \${firstName}", "Java Test 04")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("05 SMS send with data (template variables)") {
+ val accounts = listOf(
+ SmsAccount(first1, last1, phone1, customFields = mapOf("city" to "Miami", "code" to "JV5"))
+ )
+ val res = client.sms.send(accounts, "Hello \${firstName}, code \${code} from \${city}", "Java Test 05")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("06 SMS sendSingle with customData") {
+ val res = client.sms.sendSingle(
+ first1, last1, phone1,
+ "Custom data test", "Java Test 06",
+ """{"source":"java-integration"}"""
+ )
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ // -----------------------------------------------------------------------
+ // MMS Tests (07–17)
+ // -----------------------------------------------------------------------
+ println("\n--- MMS ---")
+
+ var signedUrl: String? = null
+ var fileKey: String? = null
+
+ runTest("07 MMS getSignedUploadUrl") {
+ val req = com.cloudcontactai.sdk.mms.SignedUploadUrlRequest(
+ fileName = "java_test.png",
+ fileType = "image/png",
+ publicFile = true
+ )
+ val res = client.mms.getSignedUploadUrl(req)
+ check(res.signedS3Url.isNotBlank()) { "Missing signedS3Url" }
+ signedUrl = res.signedS3Url
+ fileKey = res.fileKey
+ }
+
+ runTest("08 MMS uploadImageToSignedUrl") {
+ val url = signedUrl ?: error("Dependency test 07 failed — skipping")
+ client.mms.uploadImageToSignedUrl(url, imageFile, "image/png")
+ }
+
+ runTest("09 MMS sendSingle") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val res = client.mms.sendSingle(first1, last1, phone1, "MMS single test", "Java MMS 09", fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("10 MMS send (1 recipient)") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val accounts = listOf(MmsAccount(first1, last1, phone1))
+ val res = client.mms.send(accounts, "MMS bulk test", "Java MMS 10", fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("11 MMS send (2 recipients)") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val accounts = listOf(
+ MmsAccount(first1, last1, phone1),
+ MmsAccount(first2, last2, phone2)
+ )
+ val res = client.mms.send(accounts, "MMS 2-recipient test", "Java MMS 11", fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("12 MMS send (3 recipients)") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val accounts = listOf(
+ MmsAccount(first1, last1, phone1),
+ MmsAccount(first2, last2, phone2),
+ MmsAccount(first3, last3, phone3)
+ )
+ val res = client.mms.send(accounts, "MMS 3-recipient test", "Java MMS 12", fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("13 MMS send with data (template variables)") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val accounts = listOf(MmsAccount(first1, last1, phone1, mapOf("promo" to "JV13")))
+ val res = client.mms.send(accounts, "MMS data promo \${promo}", "Java MMS 13", fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("14 MMS sendSingle with customData") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val res = client.mms.sendSingle(
+ first1, last1, phone1,
+ "MMS custom data test", "Java MMS 14", fk,
+ """{"source":"java-integration"}"""
+ )
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("15 MMS checkFileUploaded") {
+ val fk = fileKey ?: error("Dependency test 07 failed — skipping")
+ val res = client.mms.checkFileUploaded(fk)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("16 MMS sendWithImage (fresh upload)") {
+ val accounts = listOf(MmsAccount(first1, last1, phone1))
+ val res = client.mms.sendWithImage(accounts, "MMS sendWithImage test", "Java MMS 16", imageFile)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("17 MMS sendWithImage (cached, same file)") {
+ val accounts = listOf(MmsAccount(first1, last1, phone1))
+ val res = client.mms.sendWithImage(accounts, "MMS cached image test", "Java MMS 17", imageFile)
+ checkNotNull(res) { "Null response" }
+ }
+
+ // -----------------------------------------------------------------------
+ // Email Tests (18–22)
+ // -----------------------------------------------------------------------
+ println("\n--- Email ---")
+
+ runTest("18 Email sendSingle") {
+ val res = client.email.sendSingle(
+ first1, last1, email1,
+ "Java Integration Test 18",
+ "
Hello \${firstName}!
"
+ )
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("19 Email send (1 recipient)") {
+ val accounts = listOf(EmailAccount(first1, last1, email1))
+ val res = client.email.send(accounts, "Java Integration Test 19", "Hello \${firstName}!
")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("20 Email send (2 recipients)") {
+ val accounts = listOf(
+ EmailAccount(first1, last1, email1),
+ EmailAccount(first2, last2, email2)
+ )
+ val res = client.email.send(accounts, "Java Integration Test 20", "Hello \${firstName}!
")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("21 Email send (3 recipients)") {
+ val accounts = listOf(
+ EmailAccount(first1, last1, email1),
+ EmailAccount(first2, last2, email2),
+ EmailAccount(first3, last3, email3)
+ )
+ val res = client.email.send(accounts, "Java Integration Test 21", "Hello \${firstName}!
")
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ runTest("22 Email send (full campaign — 3 recipients with data)") {
+ val accounts = listOf(
+ EmailAccount(first1, last1, email1, customFields = mapOf("plan" to "premium")),
+ EmailAccount(first2, last2, email2, customFields = mapOf("plan" to "standard")),
+ EmailAccount(first3, last3, email3, customFields = mapOf("plan" to "basic"))
+ )
+ val res = client.email.send(
+ accounts,
+ "Java Integration Test 22",
+ "Campaign Test
Hello \${firstName}, your plan is \${plan}.
",
+ senderEmail = "noreply@cloudcontactai.com",
+ replyEmail = "noreply@cloudcontactai.com",
+ senderName = "Java Integration"
+ )
+ check(res.id.isNotBlank()) { "Empty id in response" }
+ }
+
+ // -----------------------------------------------------------------------
+ // Webhook Tests (23–29)
+ // -----------------------------------------------------------------------
+ println("\n--- Webhook ---")
+
+ var webhookId: Long? = null
+
+ runTest("23 Webhook create (register)") {
+ val req = WebhookRequest(webhookUrl, "java-test-secret-key")
+ val res = client.webhook.create(req)
+ check(res.id > 0) { "Invalid webhook id: ${res.id}" }
+ webhookId = res.id
+ }
+
+ runTest("24 Webhook getAll (list)") {
+ val list = client.webhook.getAll()
+ check(list.isNotEmpty() || list.isEmpty()) { "Expected a list" } // just verify it doesn't throw
+ }
+
+ runTest("25 Webhook update") {
+ val id = webhookId ?: error("Dependency test 23 failed — skipping")
+ val req = WebhookUpdateRequest(id, "$webhookUrl/updated")
+ val res = client.webhook.update(req)
+ check(res.id > 0) { "Invalid webhook id in update response" }
+ }
+
+ runTest("26 Webhook validateSignature (valid)") {
+ val secret = "java-test-secret-key"
+ val eventHash = "abc123hash"
+ val clientIdLong = clientId.toLong()
+ val expected = client.webhook.generateSignature(secret, clientIdLong, eventHash)
+ val ok = client.webhook.validateSignature(expected, secret, clientIdLong, eventHash)
+ check(ok) { "Valid signature validation returned false" }
+ }
+
+ runTest("27 Webhook validateSignature (invalid)") {
+ val ok = client.webhook.validateSignature("invalidsig==", "wrong-secret", clientId.toLong(), "somehash")
+ check(!ok) { "Invalid signature validation returned true" }
+ }
+
+ runTest("28 Webhook parseWebhookEvent") {
+ val payload = """{"eventType":"SMS_SENT","data":{"phone":"+13055551234"},"eventHash":"abc123hash"}"""
+ val event = client.webhook.parseWebhookEvent(payload)
+ check(event.eventType.isNotBlank()) { "Missing eventType in parsed event" }
+ }
+
+ runTest("29 Webhook delete") {
+ val id = webhookId ?: error("Dependency test 23 failed — skipping")
+ val res = client.webhook.delete(id)
+ check(res.id == id) { "Deleted webhook id mismatch" }
+ }
+
+ // -----------------------------------------------------------------------
+ // Contact Tests (30–31)
+ // -----------------------------------------------------------------------
+ println("\n--- Contact ---")
+
+ runTest("30 Contact setDoNotText (opt-out)") {
+ val res = client.contact.setDoNotText(phone = phone1, doNotText = true)
+ checkNotNull(res) { "Null response" }
+ }
+
+ runTest("31 Contact setDoNotText (opt-in)") {
+ val res = client.contact.setDoNotText(phone = phone1, doNotText = false)
+ checkNotNull(res) { "Null response" }
+ }
+
+ // -----------------------------------------------------------------------
+ // Brand Tests (32–36)
+ // -----------------------------------------------------------------------
+ println("\n--- Brands ---")
+
+ var brandId: Long? = null
+
+ runTest("32 Brand create") {
+ val req = BrandRequest(
+ 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 = first1,
+ contactLastName = last1,
+ contactEmail = email1,
+ contactPhone = phone1
+ )
+ val res = client.brands.create(req)
+ check(res.id > 0) { "Invalid brand id: ${res.id}" }
+ brandId = res.id
+ }
+
+ runTest("33 Brand get") {
+ val id = brandId ?: error("Dependency test 32 failed — skipping")
+ val res = client.brands.get(id)
+ check(res.id == id) { "Brand id mismatch" }
+ }
+
+ runTest("34 Brand list") {
+ val list = client.brands.list()
+ checkNotNull(list) { "Null response" }
+ }
+
+ runTest("35 Brand update") {
+ val id = brandId ?: error("Dependency test 32 failed — skipping")
+ val req = BrandRequest(city = "Orlando")
+ val res = client.brands.update(id, req)
+ check(res.id == id) { "Brand id mismatch after update" }
+ }
+
+ runTest("36 Brand delete") {
+ val id = brandId ?: error("Dependency test 32 failed — skipping")
+ client.brands.delete(id)
+ }
+
+ // -----------------------------------------------------------------------
+ // Campaign Tests (37–42)
+ // -----------------------------------------------------------------------
+ println("\n--- Campaigns ---")
+
+ var campaignBrandId: Long? = null
+ var campaignId: Long? = null
+
+ runTest("37 Campaign setup — create brand") {
+ val req = BrandRequest(
+ 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 = first1,
+ contactLastName = last1,
+ contactEmail = email1,
+ contactPhone = phone1
+ )
+ val res = client.brands.create(req)
+ check(res.id > 0) { "Invalid brand id: ${res.id}" }
+ campaignBrandId = res.id
+ }
+
+ runTest("38 Campaign create") {
+ val bid = campaignBrandId ?: error("Dependency test 37 failed — skipping")
+ val req = CampaignRequest(
+ brandId = bid,
+ 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 = listOf("START", "YES"),
+ optInMessage = "You have opted in to receive messages. Reply STOP to unsubscribe.",
+ optInProofUrl = "https://example.com/opt-in-proof",
+ helpKeywords = listOf("HELP", "INFO"),
+ helpMessage = "For help reply HELP or call 1-800-555-0000.",
+ optOutKeywords = listOf("STOP", "END"),
+ optOutMessage = "You have been unsubscribed. Reply START to opt back in. STOP",
+ sampleMessages = listOf(
+ "Hello \${firstName}, this is a test message. Reply STOP to unsubscribe.",
+ "Reminder: your appointment is tomorrow. Reply HELP for assistance."
+ )
+ )
+ val res = client.campaigns.create(req)
+ check(res.id > 0) { "Invalid campaign id: ${res.id}" }
+ campaignId = res.id
+ }
+
+ runTest("39 Campaign get") {
+ val id = campaignId ?: error("Dependency test 38 failed — skipping")
+ val res = client.campaigns.get(id)
+ check(res.id == id) { "Campaign id mismatch" }
+ }
+
+ runTest("40 Campaign list") {
+ val list = client.campaigns.list()
+ checkNotNull(list) { "Null response" }
+ }
+
+ runTest("41 Campaign update") {
+ val id = campaignId ?: error("Dependency test 38 failed — skipping")
+ val req = CampaignRequest(description = "Updated integration test campaign description")
+ val res = client.campaigns.update(id, req)
+ check(res.id == id) { "Campaign id mismatch after update" }
+ }
+
+ runTest("42 Campaign delete") {
+ val id = campaignId ?: error("Dependency test 38 failed — skipping")
+ client.campaigns.delete(id)
+ campaignBrandId?.let { client.brands.delete(it) }
+ }
+
+ // ── Contact Validator ──────────────────────────────────────────────────────
+
+ runTest("43 ContactValidator validateEmail") {
+ val res = client.contactValidator.validateEmail(email1)
+ check(res.status.isNotBlank()) { "status is blank" }
+ }
+
+ runTest("44 ContactValidator validateEmails") {
+ val res = client.contactValidator.validateEmails(listOf(email1, email2))
+ check(res.summary.total == 2) { "expected summary.total=2, got ${res.summary.total}" }
+ }
+
+ runTest("45 ContactValidator validatePhone") {
+ val res = client.contactValidator.validatePhone(phone1)
+ check(res.status.isNotBlank()) { "status is blank" }
+ }
+
+ runTest("46 ContactValidator validatePhones") {
+ val res = client.contactValidator.validatePhones(listOf(
+ PhoneInput(phone = phone1),
+ PhoneInput(phone = phone2)
+ ))
+ check(res.summary.total == 2) { "expected summary.total=2, got ${res.summary.total}" }
+ }
+
+ // -----------------------------------------------------------------------
+ // Summary
+ // -----------------------------------------------------------------------
+ val total = passed + failed
+ println("\n=== Results: $passed/$total passed ===")
+ if (failed > 0) exitProcess(1)
+}
diff --git a/pom.xml b/pom.xml
index c45eba9..c302f6d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.cloudcontactai
ccai-java-sdk
- 1.0.5
+ 1.1.0
jar
CCAI Java SDK
@@ -123,17 +123,54 @@
org.apache.maven.plugins
- maven-javadoc-plugin
- 3.4.1
+ maven-jar-plugin
+ 3.3.0
- attach-javadocs
+ empty-javadoc-jar
+ package
jar
+
+ javadoc
+ ${project.basedir}/src/main/javadoc
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.6.0
+ true
+
+ central
+ true
+
+
@@ -141,7 +178,7 @@
central
Maven Central Repository
- https://oss.sonatype.org/service/local/staging/deploy/maven2/
+ https://central.sonatype.com
diff --git a/src/main/java/com/cloudcontactai/sdk/examples/BasicCampaignExample.java b/src/main/java/com/cloudcontactai/sdk/examples/BasicCampaignExample.java
new file mode 100644
index 0000000..3721127
--- /dev/null
+++ b/src/main/java/com/cloudcontactai/sdk/examples/BasicCampaignExample.java
@@ -0,0 +1,88 @@
+package com.cloudcontactai.sdk.examples;
+
+import com.cloudcontactai.sdk.CCAIClient;
+import com.cloudcontactai.sdk.campaigns.CampaignRequest;
+import com.cloudcontactai.sdk.campaigns.CampaignResponse;
+import com.cloudcontactai.sdk.common.CCAIConfig;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Campaign registration example using the CCAI Java SDK
+ */
+public class BasicCampaignExample {
+
+ public static void main(String[] args) {
+ CCAIConfig config = new CCAIConfig(
+ System.getenv("CCAI_CLIENT_ID"),
+ System.getenv("CCAI_API_KEY"),
+ false
+ );
+
+ CCAIClient ccai = new CCAIClient(config);
+
+ try {
+ // Create a campaign (assumes brand ID 1 exists)
+ System.out.println("Creating a campaign...");
+ CampaignResponse campaign = ccai.getCampaigns().create(new CampaignRequest(
+ 1L, // brandId
+ "MIXED", // useCase
+ Arrays.asList("CUSTOMER_CARE", "TWO_FACTOR_AUTHENTICATION", "ACCOUNT_NOTIFICATION"),
+ "This campaign handles security codes and support for Collect.org.",
+ "Users opt-in via our signup form checkbox at https://collect.org/signup",
+ "https://collect.org/terms",
+ "https://collect.org/privacy",
+ true, // hasEmbeddedLinks
+ false, // hasEmbeddedPhone
+ false, // isAgeGated
+ false, // isDirectLending
+ Arrays.asList("START", "JOIN"),
+ "Welcome to Collect.org! Msg&Data rates may apply. Reply STOP to cancel.",
+ "https://collect.org/images/opt-in-proof.png",
+ Arrays.asList("HELP", "INFO"),
+ "Collect.org: For help email support@collect.org. Reply STOP to cancel.",
+ Arrays.asList("STOP", "UNSUBSCRIBE"),
+ "Collect.org: You have been unsubscribed. STOP received.",
+ Arrays.asList(
+ "Your Collect.org security code is 554321. Reply STOP to cancel.",
+ "Hi [Name], your ticket #[ID] has been updated. Reply HELP for more info."
+ )
+ ));
+ System.out.println("Campaign created with ID: " + campaign.getId());
+
+ // Get campaign by ID
+ System.out.println("\nFetching campaign by ID...");
+ CampaignResponse fetched = ccai.getCampaigns().get(campaign.getId());
+ System.out.println("Campaign: " + fetched.getUseCase() + ", Brand: " + fetched.getBrandId());
+
+ // List all campaigns
+ System.out.println("\nListing all campaigns...");
+ CampaignResponse[] campaigns = ccai.getCampaigns().list();
+ System.out.println("Found " + campaigns.length + " campaign(s)");
+
+ // Update a campaign
+ System.out.println("\nUpdating campaign...");
+ CampaignResponse updated = ccai.getCampaigns().update(campaign.getId(), new CampaignRequest(
+ null, null, null,
+ "Updated campaign description for Collect.org messaging.",
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ Arrays.asList(
+ "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."
+ )
+ ));
+ System.out.println("Campaign updated: " + updated.getDescription());
+
+ // Delete a campaign
+ System.out.println("\nDeleting campaign...");
+ ccai.getCampaigns().delete(campaign.getId());
+ System.out.println("Campaign deleted successfully");
+
+ } catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/javadoc/README.md b/src/main/javadoc/README.md
new file mode 100644
index 0000000..2ed3e0e
--- /dev/null
+++ b/src/main/javadoc/README.md
@@ -0,0 +1 @@
+# CCAI Java SDK Documentation\nVisit https://github.com/cloudcontactai/ccai-java-sdk for documentation
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt
index 14c8d05..c87aaad 100644
--- a/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt
+++ b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt
@@ -3,20 +3,26 @@ package com.cloudcontactai.sdk
import com.cloudcontactai.sdk.common.ApiClient
import com.cloudcontactai.sdk.common.CCAIConfig
import com.cloudcontactai.sdk.contact.ContactService
+import com.cloudcontactai.sdk.contactvalidator.ContactValidatorService
import com.cloudcontactai.sdk.sms.SMSService
import com.cloudcontactai.sdk.email.EmailService
import com.cloudcontactai.sdk.webhook.WebhookService
import com.cloudcontactai.sdk.mms.MMSService
+import com.cloudcontactai.sdk.brands.BrandService
+import com.cloudcontactai.sdk.campaigns.CampaignService
class CCAIClient(private val config: CCAIConfig) {
private val apiClient = ApiClient(config)
-
+
val sms = SMSService(config, apiClient)
val email = EmailService(config, apiClient)
val webhook = WebhookService(config, apiClient)
val mms = MMSService(config, apiClient)
val contact = ContactService(config, apiClient)
-
+ val brands = BrandService(config, apiClient)
+ val campaigns = CampaignService(config, apiClient)
+ val contactValidator = ContactValidatorService(config, apiClient)
+
fun close() {
// Cleanup resources if needed
}
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/ExampleRunner.kt b/src/main/kotlin/com/cloudcontactai/sdk/ExampleRunner.kt
index 51014e1..28ac7f5 100644
--- a/src/main/kotlin/com/cloudcontactai/sdk/ExampleRunner.kt
+++ b/src/main/kotlin/com/cloudcontactai/sdk/ExampleRunner.kt
@@ -3,6 +3,8 @@ package com.cloudcontactai.sdk
import com.cloudcontactai.sdk.common.CCAIConfig
import com.cloudcontactai.sdk.webhook.WebhookRequest
import com.cloudcontactai.sdk.webhook.WebhookUpdateRequest
+import com.cloudcontactai.sdk.brands.BrandRequest
+import com.cloudcontactai.sdk.campaigns.CampaignRequest
import com.cloudcontactai.sdk.mms.Account as MMSAccount
import java.io.File
@@ -21,6 +23,8 @@ fun main() {
runSampleMMS(ccai)
runSampleContactDoNotText(ccai)
runSampleWebhook(ccai)
+ runSampleBrands(ccai)
+ runSampleCampaigns(ccai)
} catch (e: Exception) {
println("Error: ${e.message}")
e.printStackTrace()
@@ -156,3 +160,112 @@ fun runSampleWebhook(ccai: CCAIClient){
val webhookDeleted = ccai.webhook.delete(webhookResponse.id)
println("Deleted webhook with ID: ${webhookDeleted.id}")
}
+
+fun runSampleBrands(ccai: CCAIClient){
+ println("\n=== Brand Registration Examples ===")
+
+ // Create a brand
+ println("\nCreating a brand")
+ val brand = ccai.brands.create(BrandRequest(
+ 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"
+ ))
+ println("Brand created with ID: ${brand.id}")
+
+ // Get brand by ID
+ println("\nFetching brand by ID")
+ val fetched = ccai.brands.get(brand.id)
+ println("Brand: ${fetched.legalCompanyName}, Score: ${fetched.websiteMatchScore ?: "pending"}")
+
+ // List all brands
+ println("\nListing all brands")
+ val brands = ccai.brands.list()
+ println("Found ${brands.size} brand(s)")
+
+ // Update a brand
+ println("\nUpdating brand")
+ val updated = ccai.brands.update(brand.id, BrandRequest(
+ street = "456 Oak Avenue",
+ city = "Los Angeles",
+ contactEmail = "admin@collect.org"
+ ))
+ println("Brand updated: ${updated.street}, ${updated.city}")
+
+ // Delete a brand
+ println("\nDeleting brand")
+ ccai.brands.delete(brand.id)
+ println("Brand deleted successfully")
+}
+
+fun runSampleCampaigns(ccai: CCAIClient) {
+ println("\n=== Campaign Registration Examples ===")
+
+ // Create a campaign (assumes brand ID 1 exists)
+ println("\nCreating a campaign")
+ val campaign = ccai.campaigns.create(CampaignRequest(
+ brandId = 1L,
+ useCase = "MIXED",
+ subUseCases = listOf("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 = listOf("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 = listOf("HELP", "INFO"),
+ helpMessage = "Collect.org: For help email support@collect.org. Reply STOP to cancel.",
+ optOutKeywords = listOf("STOP", "UNSUBSCRIBE"),
+ optOutMessage = "Collect.org: You have been unsubscribed. STOP received.",
+ sampleMessages = listOf(
+ "Your Collect.org security code is 554321. Reply STOP to cancel.",
+ "Hi [Name], your ticket #[ID] has been updated. Reply HELP for more info."
+ )
+ ))
+ println("Campaign created with ID: ${campaign.id}")
+
+ // Get campaign by ID
+ println("\nFetching campaign by ID")
+ val fetched = ccai.campaigns.get(campaign.id)
+ println("Campaign: ${fetched.useCase}, Brand: ${fetched.brandId}")
+
+ // List all campaigns
+ println("\nListing all campaigns")
+ val campaigns = ccai.campaigns.list()
+ println("Found ${campaigns.size} campaign(s)")
+
+ // Update a campaign
+ println("\nUpdating campaign")
+ val updated = ccai.campaigns.update(campaign.id, CampaignRequest(
+ description = "Updated campaign description for Collect.org messaging.",
+ sampleMessages = listOf(
+ "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."
+ )
+ ))
+ println("Campaign updated: ${updated.description}")
+
+ // Delete a campaign
+ println("\nDeleting campaign")
+ ccai.campaigns.delete(campaign.id)
+ println("Campaign deleted successfully")
+}
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandModels.kt b/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandModels.kt
new file mode 100644
index 0000000..47e2b74
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandModels.kt
@@ -0,0 +1,53 @@
+package com.cloudcontactai.sdk.brands
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class BrandRequest(
+ val legalCompanyName: String? = null,
+ val dba: String? = null,
+ val entityType: String? = null,
+ val taxId: String? = null,
+ val taxIdCountry: String? = null,
+ val country: String? = null,
+ val verticalType: String? = null,
+ val websiteUrl: String? = null,
+ val stockSymbol: String? = null,
+ val stockExchange: String? = null,
+ val street: String? = null,
+ val city: String? = null,
+ val state: String? = null,
+ val postalCode: String? = null,
+ val contactFirstName: String? = null,
+ val contactLastName: String? = null,
+ val contactEmail: String? = null,
+ val contactPhone: String? = null,
+ val websiteMatch: Boolean = false
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class BrandResponse(
+ val id: Long = 0,
+ val accountId: Long = 0,
+ val legalCompanyName: String = "",
+ val dba: String? = null,
+ val entityType: String = "",
+ val taxId: String = "",
+ val taxIdCountry: String = "",
+ val country: String = "",
+ val verticalType: String = "",
+ val websiteUrl: String = "",
+ val stockSymbol: String? = null,
+ val stockExchange: String? = null,
+ val street: String = "",
+ val city: String = "",
+ val state: String = "",
+ val postalCode: String = "",
+ val contactFirstName: String = "",
+ val contactLastName: String = "",
+ val contactEmail: String = "",
+ val contactPhone: String = "",
+ val websiteMatchScore: Int? = null,
+ val createdAt: String = "",
+ val updatedAt: String = ""
+)
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandService.kt b/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandService.kt
new file mode 100644
index 0000000..c404717
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/brands/BrandService.kt
@@ -0,0 +1,116 @@
+package com.cloudcontactai.sdk.brands
+
+import com.cloudcontactai.sdk.common.ApiClient
+import com.cloudcontactai.sdk.common.CCAIConfig
+import com.cloudcontactai.sdk.common.CCAIException
+
+class BrandService(private val config: CCAIConfig, private val apiClient: ApiClient) {
+
+ companion object {
+ private val ENTITY_TYPES = setOf("PRIVATE_PROFIT", "PUBLIC_PROFIT", "NON_PROFIT", "GOVERNMENT", "SOLE_PROPRIETOR")
+ private val VERTICAL_TYPES = setOf(
+ "AUTOMOTIVE", "AGRICULTURE", "BANKING", "COMMUNICATION", "CONSTRUCTION", "EDUCATION",
+ "ENERGY", "ENTERTAINMENT", "GOVERNMENT", "HEALTHCARE", "HOSPITALITY", "INSURANCE",
+ "LEGAL", "MANUFACTURING", "NON_PROFIT", "PROFESSIONAL", "REAL_ESTATE", "RETAIL",
+ "TECHNOLOGY", "TRANSPORTATION"
+ )
+ private val TAX_ID_COUNTRIES = setOf("US", "CA", "GB", "AU")
+ private val STOCK_EXCHANGES = setOf("NASDAQ", "NYSE", "AMEX", "TSX", "LON", "JPX", "HKEX", "OTHER")
+ }
+
+ fun create(data: BrandRequest): BrandResponse {
+ validate(data, isCreate = true)
+ return apiClient.request(
+ method = "POST",
+ endpoint = "/v1/brands",
+ data = data,
+ baseUrl = config.complianceBaseUrl,
+ responseClass = BrandResponse::class.java
+ )
+ }
+
+ fun get(id: Long): BrandResponse {
+ return apiClient.request(
+ method = "GET",
+ endpoint = "/v1/brands/$id",
+ baseUrl = config.complianceBaseUrl,
+ responseClass = BrandResponse::class.java
+ )
+ }
+
+ fun list(): Array {
+ return apiClient.request(
+ method = "GET",
+ endpoint = "/v1/brands",
+ baseUrl = config.complianceBaseUrl,
+ responseClass = Array::class.java
+ )
+ }
+
+ fun update(id: Long, data: BrandRequest): BrandResponse {
+ validate(data, isCreate = false)
+ return apiClient.request(
+ method = "PATCH",
+ endpoint = "/v1/brands/$id",
+ data = data,
+ baseUrl = config.complianceBaseUrl,
+ responseClass = BrandResponse::class.java
+ )
+ }
+
+ fun delete(id: Long) {
+ apiClient.requestNoContent(
+ method = "DELETE",
+ endpoint = "/v1/brands/$id",
+ baseUrl = config.complianceBaseUrl
+ )
+ }
+
+ private fun validate(data: BrandRequest, isCreate: Boolean) {
+ val errors = mutableListOf()
+
+ if (isCreate) {
+ if (data.legalCompanyName.isNullOrBlank()) errors.add("legalCompanyName is required")
+ if (data.entityType.isNullOrBlank()) errors.add("entityType is required")
+ if (data.taxId.isNullOrBlank()) errors.add("taxId is required")
+ if (data.taxIdCountry.isNullOrBlank()) errors.add("taxIdCountry is required")
+ if (data.country.isNullOrBlank()) errors.add("country is required")
+ if (data.verticalType.isNullOrBlank()) errors.add("verticalType is required")
+ if (data.websiteUrl.isNullOrBlank()) errors.add("websiteUrl is required")
+ if (data.street.isNullOrBlank()) errors.add("street is required")
+ if (data.city.isNullOrBlank()) errors.add("city is required")
+ if (data.state.isNullOrBlank()) errors.add("state is required")
+ if (data.postalCode.isNullOrBlank()) errors.add("postalCode is required")
+ if (data.contactFirstName.isNullOrBlank()) errors.add("contactFirstName is required")
+ if (data.contactLastName.isNullOrBlank()) errors.add("contactLastName is required")
+ if (data.contactEmail.isNullOrBlank()) errors.add("contactEmail is required")
+ if (data.contactPhone.isNullOrBlank()) errors.add("contactPhone is required")
+ }
+
+ data.entityType?.let { if (it !in ENTITY_TYPES) errors.add("Invalid entity type") }
+ data.verticalType?.let { if (it !in VERTICAL_TYPES) errors.add("Invalid vertical type") }
+ data.taxIdCountry?.let { if (it !in TAX_ID_COUNTRIES) errors.add("Invalid tax ID country") }
+ data.stockExchange?.let { if (it !in STOCK_EXCHANGES) errors.add("Invalid stock exchange") }
+
+ data.websiteUrl?.let {
+ if (!it.startsWith("http://") && !it.startsWith("https://")) errors.add("Website URL must start with http:// or https://")
+ }
+
+ data.contactEmail?.let {
+ if (!it.matches(Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))) errors.add("Invalid email format")
+ }
+
+ if (data.taxId != null && data.taxIdCountry != null && data.taxIdCountry in setOf("US", "CA")) {
+ if (!data.taxId.matches(Regex("^\\d{9}$"))) errors.add("Tax ID must be exactly 9 digits for ${data.taxIdCountry}")
+ }
+
+ if (data.entityType == "PUBLIC_PROFIT") {
+ if (data.stockSymbol.isNullOrBlank()) errors.add("Stock symbol is required for PUBLIC_PROFIT entities")
+ if (data.stockExchange.isNullOrBlank()) errors.add("Stock exchange is required for PUBLIC_PROFIT entities")
+ }
+
+ if (errors.isNotEmpty()) {
+ throw CCAIException("Validation failed: ${errors.joinToString(", ")}")
+ }
+ }
+}
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignModels.kt b/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignModels.kt
new file mode 100644
index 0000000..71c59b6
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignModels.kt
@@ -0,0 +1,53 @@
+package com.cloudcontactai.sdk.campaigns
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+data class CampaignRequest(
+ val brandId: Long? = null,
+ val useCase: String? = null,
+ val subUseCases: List? = null,
+ val description: String? = null,
+ val messageFlow: String? = null,
+ val termsLink: String? = null,
+ val privacyLink: String? = null,
+ val hasEmbeddedLinks: Boolean? = null,
+ val hasEmbeddedPhone: Boolean? = null,
+ val isAgeGated: Boolean? = null,
+ val isDirectLending: Boolean? = null,
+ val optInKeywords: List? = null,
+ val optInMessage: String? = null,
+ val optInProofUrl: String? = null,
+ val helpKeywords: List? = null,
+ val helpMessage: String? = null,
+ val optOutKeywords: List? = null,
+ val optOutMessage: String? = null,
+ val sampleMessages: List? = null
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class CampaignResponse(
+ val id: Long = 0,
+ val accountId: Long = 0,
+ val brandId: Long = 0,
+ val useCase: String = "",
+ val subUseCases: List = emptyList(),
+ val description: String = "",
+ val messageFlow: String = "",
+ val termsLink: String? = null,
+ val privacyLink: String? = null,
+ val hasEmbeddedLinks: Boolean = false,
+ val hasEmbeddedPhone: Boolean = false,
+ val isAgeGated: Boolean = false,
+ val isDirectLending: Boolean = false,
+ val optInKeywords: List = emptyList(),
+ val optInMessage: String = "",
+ val optInProofUrl: String = "",
+ val helpKeywords: List = emptyList(),
+ val helpMessage: String = "",
+ val optOutKeywords: List = emptyList(),
+ val optOutMessage: String = "",
+ val sampleMessages: List = emptyList(),
+ val monthlyFee: Double = 20.00,
+ val createdAt: String = "",
+ val updatedAt: String = ""
+)
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignService.kt b/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignService.kt
new file mode 100644
index 0000000..cfb35f8
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignService.kt
@@ -0,0 +1,162 @@
+package com.cloudcontactai.sdk.campaigns
+
+import com.cloudcontactai.sdk.common.ApiClient
+import com.cloudcontactai.sdk.common.CCAIConfig
+import com.cloudcontactai.sdk.common.CCAIException
+
+class CampaignService(private val config: CCAIConfig, private val apiClient: ApiClient) {
+
+ companion object {
+ private val CAMPAIGN_USE_CASES = setOf(
+ "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"
+ )
+ private val CAMPAIGN_SUB_USE_CASES = setOf(
+ "TWO_FACTOR_AUTHENTICATION", "ACCOUNT_NOTIFICATION", "CUSTOMER_CARE", "DELIVERY_NOTIFICATION",
+ "FRAUD_ALERT", "MARKETING", "POLLING_VOTING"
+ )
+ private val MIXED_USE_CASES = setOf("MIXED", "LOW_VOLUME_MIXED")
+ }
+
+ fun create(data: CampaignRequest): CampaignResponse {
+ validate(data, isCreate = true)
+ return apiClient.request(
+ method = "POST",
+ endpoint = "/v1/campaigns",
+ data = data,
+ baseUrl = config.complianceBaseUrl,
+ responseClass = CampaignResponse::class.java
+ )
+ }
+
+ fun get(id: Long): CampaignResponse {
+ return apiClient.request(
+ method = "GET",
+ endpoint = "/v1/campaigns/$id",
+ baseUrl = config.complianceBaseUrl,
+ responseClass = CampaignResponse::class.java
+ )
+ }
+
+ fun list(): Array {
+ return apiClient.request(
+ method = "GET",
+ endpoint = "/v1/campaigns",
+ baseUrl = config.complianceBaseUrl,
+ responseClass = Array::class.java
+ )
+ }
+
+ fun update(id: Long, data: CampaignRequest): CampaignResponse {
+ validate(data, isCreate = false)
+ return apiClient.request(
+ method = "PATCH",
+ endpoint = "/v1/campaigns/$id",
+ data = data,
+ baseUrl = config.complianceBaseUrl,
+ responseClass = CampaignResponse::class.java
+ )
+ }
+
+ fun delete(id: Long) {
+ apiClient.requestNoContent(
+ method = "DELETE",
+ endpoint = "/v1/campaigns/$id",
+ baseUrl = config.complianceBaseUrl
+ )
+ }
+
+ private fun validate(data: CampaignRequest, isCreate: Boolean) {
+ val errors = mutableListOf()
+
+ if (isCreate) {
+ if (data.brandId == null) errors.add("brandId is required")
+ if (data.useCase.isNullOrBlank()) errors.add("useCase is required")
+ if (data.description.isNullOrBlank()) errors.add("description is required")
+ if (data.messageFlow.isNullOrBlank()) errors.add("messageFlow is required")
+ if (data.hasEmbeddedLinks == null) errors.add("hasEmbeddedLinks is required")
+ if (data.hasEmbeddedPhone == null) errors.add("hasEmbeddedPhone is required")
+ if (data.isAgeGated == null) errors.add("isAgeGated is required")
+ if (data.isDirectLending == null) errors.add("isDirectLending is required")
+ if (data.optInKeywords.isNullOrEmpty()) errors.add("optInKeywords is required")
+ if (data.optInMessage.isNullOrBlank()) errors.add("optInMessage is required")
+ if (data.optInProofUrl.isNullOrBlank()) errors.add("optInProofUrl is required")
+ if (data.helpKeywords.isNullOrEmpty()) errors.add("helpKeywords is required")
+ if (data.helpMessage.isNullOrBlank()) errors.add("helpMessage is required")
+ if (data.optOutKeywords.isNullOrEmpty()) errors.add("optOutKeywords is required")
+ if (data.optOutMessage.isNullOrBlank()) errors.add("optOutMessage is required")
+ if (data.sampleMessages.isNullOrEmpty()) errors.add("sampleMessages is required")
+ }
+
+ data.useCase?.let { if (it !in CAMPAIGN_USE_CASES) errors.add("Invalid use case") }
+
+ // MIXED/LOW_VOLUME_MIXED sub-use case validation
+ val useCase = data.useCase
+ val subUseCases = data.subUseCases
+ if (useCase != null && useCase in MIXED_USE_CASES) {
+ if (subUseCases == null || subUseCases.size < 2 || subUseCases.size > 3) {
+ errors.add("MIXED/LOW_VOLUME_MIXED requires 2-3 sub use cases")
+ } else {
+ subUseCases.forEach { if (it !in CAMPAIGN_SUB_USE_CASES) errors.add("Invalid sub use case: $it") }
+ }
+ } else if (useCase != null && !subUseCases.isNullOrEmpty()) {
+ errors.add("subUseCases should be empty for non-MIXED use cases")
+ }
+
+ // sampleMessages count validation
+ if (data.sampleMessages != null) {
+ val msgs = data.sampleMessages!!
+ if (msgs.size < 2 || msgs.size > 5) {
+ errors.add("sampleMessages must contain 2-5 items")
+ } else {
+ val optOutKws = data.optOutKeywords ?: emptyList()
+ val helpKws = data.helpKeywords ?: emptyList()
+
+ val hasOptOut = msgs.any { msg ->
+ msg.contains("Reply STOP") || optOutKws.any { kw -> msg.contains("Reply $kw") }
+ }
+ if (!hasOptOut) errors.add("At least one sample must contain 'Reply STOP' or 'Reply {optOutKeyword}'")
+
+ val hasHelp = msgs.any { msg ->
+ msg.contains("Reply HELP") || helpKws.any { kw -> msg.contains("Reply $kw") }
+ }
+ if (!hasHelp) errors.add("At least one sample must contain 'Reply HELP' or 'Reply {helpKeyword}'")
+ }
+ }
+
+ // optOutMessage must contain STOP or an opt-out keyword
+ if (data.optOutMessage != null) {
+ val msg = data.optOutMessage!!
+ val optOutKws = data.optOutKeywords ?: emptyList()
+ val hasKeyword = optOutKws.any { kw -> msg.contains(kw) }
+ if (!msg.contains("STOP") && !hasKeyword) {
+ errors.add("optOutMessage must contain 'STOP' or at least one optOutKeyword")
+ }
+ }
+
+ // helpMessage must contain HELP or a help keyword
+ if (data.helpMessage != null) {
+ val msg = data.helpMessage!!
+ val helpKws = data.helpKeywords ?: emptyList()
+ val hasKeyword = helpKws.any { kw -> msg.contains(kw) }
+ if (!msg.contains("HELP") && !hasKeyword) {
+ errors.add("helpMessage must contain 'HELP' or at least one helpKeyword")
+ }
+ }
+
+ data.optInProofUrl?.let {
+ if (!it.startsWith("http://") && !it.startsWith("https://")) errors.add("Opt-in proof URL must start with http:// or https://")
+ }
+ data.termsLink?.let {
+ if (!it.startsWith("http://") && !it.startsWith("https://")) errors.add("Terms link must start with http:// or https://")
+ }
+ data.privacyLink?.let {
+ if (!it.startsWith("http://") && !it.startsWith("https://")) errors.add("Privacy link must start with http:// or https://")
+ }
+
+ if (errors.isNotEmpty()) {
+ throw CCAIException("Validation failed: ${errors.joinToString(", ")}")
+ }
+ }
+}
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/common/ApiClient.kt b/src/main/kotlin/com/cloudcontactai/sdk/common/ApiClient.kt
index a430985..c3c8352 100644
--- a/src/main/kotlin/com/cloudcontactai/sdk/common/ApiClient.kt
+++ b/src/main/kotlin/com/cloudcontactai/sdk/common/ApiClient.kt
@@ -50,6 +50,14 @@ class ApiClient(config: CCAIConfig) {
}
requestBuilder.put(body)
}
+ "PATCH" -> {
+ val body = if (data != null) {
+ objectMapper.writeValueAsString(data).toRequestBody(jsonMediaType)
+ } else {
+ "".toRequestBody(jsonMediaType)
+ }
+ requestBuilder.patch(body)
+ }
"DELETE" -> requestBuilder.delete()
}
@@ -66,6 +74,39 @@ class ApiClient(config: CCAIConfig) {
return objectMapper.readValue(responseBody, responseClass)
}
}
+
+ fun requestNoContent(
+ method: String,
+ endpoint: String,
+ data: Any? = null,
+ baseUrl: String? = null,
+ headers: Map = emptyMap()
+ ) {
+ val url = "${baseUrl ?: this.baseUrl}$endpoint"
+
+ val requestBuilder = Request.Builder()
+ .url(url)
+ .addHeader("Authorization", "Bearer $apiKey")
+ .addHeader("Accept", "application/json")
+
+ headers.forEach { (key, value) ->
+ requestBuilder.addHeader(key, value)
+ }
+
+ when (method.uppercase()) {
+ "DELETE" -> requestBuilder.delete()
+ else -> {}
+ }
+
+ val request = requestBuilder.build()
+
+ client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) {
+ val errorBody = response.body?.string() ?: ""
+ throw CCAIException("HTTP ${response.code}: ${response.message} $errorBody")
+ }
+ }
+ }
}
inline fun ApiClient.request(
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/common/CCAIConfig.kt b/src/main/kotlin/com/cloudcontactai/sdk/common/CCAIConfig.kt
index 626584a..53500a4 100644
--- a/src/main/kotlin/com/cloudcontactai/sdk/common/CCAIConfig.kt
+++ b/src/main/kotlin/com/cloudcontactai/sdk/common/CCAIConfig.kt
@@ -77,6 +77,16 @@ data class CCAIConfig @JvmOverloads constructor(
System.getenv("CCAI_FILES_BASE_URL") ?: "https://files.cloudcontactai.com"
}
+ /**
+ * Base URL for the Compliance API (Brands/Campaigns)
+ */
+ val complianceBaseUrl: String = if (useTestEnvironment) {
+ System.getenv("CCAI_COMPLIANCE_BASE_URL")?.replace("compliance.cloudcontactai.com", "compliance-test-cloudcontactai.allcode.com")
+ ?: "https://compliance-test-cloudcontactai.allcode.com/api"
+ } else {
+ System.getenv("CCAI_COMPLIANCE_BASE_URL") ?: "https://compliance.cloudcontactai.com/api"
+ }
+
init {
require(clientId.isNotBlank()) { "Client ID cannot be blank" }
require(apiKey.isNotBlank()) { "API key cannot be blank" }
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorModels.kt b/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorModels.kt
new file mode 100644
index 0000000..6e2e379
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorModels.kt
@@ -0,0 +1,38 @@
+package com.cloudcontactai.sdk.contactvalidator
+
+data class EmailValidationResult(
+ val contact: String,
+ val type: String,
+ val status: String,
+ val metadata: Map = emptyMap()
+)
+
+data class PhoneValidationResult(
+ val contact: String,
+ val type: String,
+ val status: String,
+ val metadata: Map = emptyMap()
+)
+
+data class ValidationSummary(
+ val total: Int,
+ val valid: Int,
+ val invalid: Int,
+ val risky: Int,
+ val landline: Int = 0
+)
+
+data class BulkEmailValidationResult(
+ val results: List,
+ val summary: ValidationSummary
+)
+
+data class BulkPhoneValidationResult(
+ val results: List,
+ val summary: ValidationSummary
+)
+
+data class PhoneInput(
+ val phone: String,
+ val countryCode: String? = null
+)
diff --git a/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorService.kt b/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorService.kt
new file mode 100644
index 0000000..b74e5ac
--- /dev/null
+++ b/src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorService.kt
@@ -0,0 +1,39 @@
+package com.cloudcontactai.sdk.contactvalidator
+
+import com.cloudcontactai.sdk.common.ApiClient
+import com.cloudcontactai.sdk.common.CCAIConfig
+
+class ContactValidatorService(private val config: CCAIConfig, private val apiClient: ApiClient) {
+
+ fun validateEmail(email: String): EmailValidationResult =
+ apiClient.request(
+ method = "POST",
+ endpoint = "/v1/contact-validator/email",
+ data = mapOf("email" to email),
+ responseClass = EmailValidationResult::class.java
+ )
+
+ fun validateEmails(emails: List): BulkEmailValidationResult =
+ apiClient.request(
+ method = "POST",
+ endpoint = "/v1/contact-validator/emails",
+ data = mapOf("emails" to emails),
+ responseClass = BulkEmailValidationResult::class.java
+ )
+
+ fun validatePhone(phone: String, countryCode: String? = null): PhoneValidationResult =
+ apiClient.request(
+ method = "POST",
+ endpoint = "/v1/contact-validator/phone",
+ data = mapOf("phone" to phone, "countryCode" to countryCode),
+ responseClass = PhoneValidationResult::class.java
+ )
+
+ fun validatePhones(phones: List): BulkPhoneValidationResult =
+ apiClient.request(
+ method = "POST",
+ endpoint = "/v1/contact-validator/phones",
+ data = mapOf("phones" to phones),
+ responseClass = BulkPhoneValidationResult::class.java
+ )
+}