From a41d9b4c2b035546a49bb6d78dcf8d1ec96be20a Mon Sep 17 00:00:00 2001 From: Mike Alvarez Date: Tue, 5 May 2026 14:15:55 -0500 Subject: [PATCH 1/9] Adding support for campaigns and brands management. --- .gitignore | 3 + README.md | 104 +++++++++++ .../javasdk/JavaSdkTestApplication.java | 111 ++++++++++++ .../sdk/examples/BasicCampaignExample.java | 88 ++++++++++ .../com/cloudcontactai/sdk/CCAIClient.kt | 4 + .../com/cloudcontactai/sdk/ExampleRunner.kt | 113 ++++++++++++ .../cloudcontactai/sdk/brands/BrandModels.kt | 53 ++++++ .../cloudcontactai/sdk/brands/BrandService.kt | 116 +++++++++++++ .../sdk/campaigns/CampaignModels.kt | 53 ++++++ .../sdk/campaigns/CampaignService.kt | 162 ++++++++++++++++++ .../cloudcontactai/sdk/common/ApiClient.kt | 41 +++++ .../cloudcontactai/sdk/common/CCAIConfig.kt | 10 ++ 12 files changed, 858 insertions(+) create mode 100644 src/main/java/com/cloudcontactai/sdk/examples/BasicCampaignExample.java create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/brands/BrandModels.kt create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/brands/BrandService.kt create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignModels.kt create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/campaigns/CampaignService.kt 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..e9c5f09 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ 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 - Variable substitution in messages @@ -330,6 +332,108 @@ 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` + ### Java Usage ```java 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/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/kotlin/com/cloudcontactai/sdk/CCAIClient.kt b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt index 14c8d05..8ec52f5 100644 --- a/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt +++ b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt @@ -7,6 +7,8 @@ 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) @@ -16,6 +18,8 @@ class CCAIClient(private val config: CCAIConfig) { 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) 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" } From ee7303aa07bf810fd9ad0fbd3c993def2d81f082 Mon Sep 17 00:00:00 2001 From: Mike Alvarez Date: Fri, 8 May 2026 10:07:53 -0500 Subject: [PATCH 2/9] Adding the workflow to deploy the releases to maven central. --- .github/workflows/publish.yml | 58 +++++++++++++++++++++++++++++++++++ pom.xml | 34 +++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..44ebde8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,58 @@ +name: Publish to Maven Central + +on: + push: + tags: + - '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/pom.xml b/pom.xml index c45eba9..c476178 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,38 @@ + + + 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 +173,7 @@ central Maven Central Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://central.sonatype.com From d6fc9180185f57fcef1df626ab0a345e3ec95ae0 Mon Sep 17 00:00:00 2001 From: Mike Alvarez Date: Fri, 8 May 2026 10:16:12 -0500 Subject: [PATCH 3/9] Upgrading version --- .github/workflows/publish.yml | 2 +- README.md | 4 ++-- create-central-bundle.sh | 18 +++++++++--------- pom.xml | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 44ebde8..00768bb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish to Maven Central on: push: tags: - - 'v*' + - 'release/v*' workflow_dispatch: jobs: diff --git a/README.md b/README.md index e9c5f09..8eba097 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add this dependency to your `pom.xml`: com.cloudcontactai ccai-java-sdk - 1.0.5 + 1.0.9 ``` @@ -40,7 +40,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 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/pom.xml b/pom.xml index c476178..4eb55d5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.cloudcontactai ccai-java-sdk - 1.0.5 + 1.0.9 jar CCAI Java SDK From 259a0b61d01f42495300b1350fe6854d02c469fd Mon Sep 17 00:00:00 2001 From: Mike Alvarez Date: Fri, 8 May 2026 10:40:07 -0500 Subject: [PATCH 4/9] Adding dummy javadoc so maven central uses it and points to the actual documentation of the library. --- pom.xml | 11 ++++++++--- src/main/javadoc/README.md | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/main/javadoc/README.md diff --git a/pom.xml b/pom.xml index 4eb55d5..17146bb 100644 --- a/pom.xml +++ b/pom.xml @@ -123,14 +123,19 @@ 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 + 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 From 6f0e654203d01009b90055c4f463cd2b93a1dd50 Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Fri, 8 May 2026 15:43:04 -0500 Subject: [PATCH 5/9] added missing documentation --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8eba097..caec647 100644 --- a/README.md +++ b/README.md @@ -247,15 +247,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") @@ -264,17 +261,50 @@ 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" +) ``` #### 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")) @@ -288,20 +318,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 = """ { @@ -434,6 +471,21 @@ ccai.campaigns.delete(campaign.id) **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 From 326469b831ff699060de875da933c83d0a853d73 Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Thu, 14 May 2026 17:45:13 -0500 Subject: [PATCH 6/9] https://mobileaws.atlassian.net/browse/CLOUD-2744 --- README.md | 33 ++++++++++++++++ pom.xml | 2 +- .../com/cloudcontactai/sdk/CCAIClient.kt | 6 ++- .../ContactValidatorModels.kt | 38 ++++++++++++++++++ .../ContactValidatorService.kt | 39 +++++++++++++++++++ 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorModels.kt create mode 100644 src/main/kotlin/com/cloudcontactai/sdk/contactvalidator/ContactValidatorService.kt diff --git a/README.md b/README.md index caec647..d94d6af 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A Kotlin/Java client library for interacting with the [CloudContactAI](https://c - 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 @@ -300,6 +301,38 @@ val singleResponse = ccai.mms.sendSingle( ) ``` +#### 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 diff --git a/pom.xml b/pom.xml index 17146bb..c302f6d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.cloudcontactai ccai-java-sdk - 1.0.9 + 1.1.0 jar CCAI Java SDK diff --git a/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt index 8ec52f5..c87aaad 100644 --- a/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt +++ b/src/main/kotlin/com/cloudcontactai/sdk/CCAIClient.kt @@ -3,6 +3,7 @@ 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 @@ -12,7 +13,7 @@ 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) @@ -20,7 +21,8 @@ class CCAIClient(private val config: CCAIConfig) { 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/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 + ) +} From 8948df5cf65034c8d9cc3b45784765aa371141df Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Fri, 1 May 2026 16:51:30 -0500 Subject: [PATCH 7/9] https://mobileaws.atlassian.net/browse/CLOUD-2733 --- .dockerignore | 9 + integration/Dockerfile | 20 + integration/pom.xml | 77 +++ .../kotlin/com/ccai/integration/TestMain.kt | 493 ++++++++++++++++++ 4 files changed, 599 insertions(+) create mode 100644 .dockerignore create mode 100644 integration/Dockerfile create mode 100644 integration/pom.xml create mode 100644 integration/src/main/kotlin/com/ccai/integration/TestMain.kt 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/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/pom.xml b/integration/pom.xml new file mode 100644 index 0000000..0be2b30 --- /dev/null +++ b/integration/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + com.ccai + ccai-integration-test-java + 1.0 + jar + + + 1.9.0 + 11 + 11 + UTF-8 + + + + + com.cloudcontactai + ccai-java-sdk + 1.0.5 + + + 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..7172273 --- /dev/null +++ b/integration/src/main/kotlin/com/ccai/integration/TestMain.kt @@ -0,0 +1,493 @@ +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 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) + } + + val config = CCAIConfig( + clientId = clientId, + apiKey = apiKey, + useTestEnvironment = true + ) + 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) } + } + + // ----------------------------------------------------------------------- + // Summary + // ----------------------------------------------------------------------- + val total = passed + failed + println("\n=== Results: $passed/$total passed ===") + if (failed > 0) exitProcess(1) +} From 743e28f1b284d245b34d4b19a4ae46ccb810a420 Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Fri, 8 May 2026 14:51:11 -0500 Subject: [PATCH 8/9] adding support to test with package repositories --- integration/Dockerfile.release | 17 +++++++++++++++++ integration/pom.xml | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 integration/Dockerfile.release 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 index 0be2b30..07c31c1 100644 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -14,13 +14,15 @@ 11 11 UTF-8 + + 1.0.5 com.cloudcontactai ccai-java-sdk - 1.0.5 + ${ccai.sdk.version} org.jetbrains.kotlin From a98b52c9cbf403fe599e64c4b1cbf7d3e02e3fee Mon Sep 17 00:00:00 2001 From: Deyner lopez Date: Thu, 14 May 2026 18:32:24 -0500 Subject: [PATCH 9/9] feat: test for email and phone validation --- .../kotlin/com/ccai/integration/TestMain.kt | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/integration/src/main/kotlin/com/ccai/integration/TestMain.kt b/integration/src/main/kotlin/com/ccai/integration/TestMain.kt index 7172273..e398ab6 100644 --- a/integration/src/main/kotlin/com/ccai/integration/TestMain.kt +++ b/integration/src/main/kotlin/com/ccai/integration/TestMain.kt @@ -9,6 +9,7 @@ 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 @@ -61,10 +62,11 @@ fun main() { exitProcess(1) } + // Use CCAI_BASE_URL if set (local dev), otherwise fall back to test environment val config = CCAIConfig( clientId = clientId, apiKey = apiKey, - useTestEnvironment = true + useTestEnvironment = System.getenv("CCAI_BASE_URL") == null ) val client = CCAIClient(config) @@ -484,6 +486,31 @@ fun main() { 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 // -----------------------------------------------------------------------