diff --git a/.github/workflows/projects.json b/.github/workflows/projects.json index d5fe1a49..e07f795a 100644 --- a/.github/workflows/projects.json +++ b/.github/workflows/projects.json @@ -8,6 +8,12 @@ { "path": "integrator-default-profile/samples/signup-sfchatsync" }, + { + "path": "ballerina-integrator/salesforce-closewon-contract-automation" + }, + { + "path": "ballerina-integrator/salesforce-leads-to-googlesheets" + }, { "path": "integrator-default-profile/samples/salesforce-leads-to-googlesheets" }, diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/config-schema.json b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/config-schema.json new file mode 100644 index 00000000..9be2126c --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/config-schema.json @@ -0,0 +1,321 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "ballerina": { + "type": "object", + "properties": { + "log": { + "type": "object", + "properties": { + "destinations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "enum": [ + "stderr", + "stdout" + ] + } + }, + "additionalProperties": false, + "required": [ + "type" + ], + "name": "ballerina/log:2.14.0:StandardDestination" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + }, + "mode": { + "enum": [ + "APPEND", + "TRUNCATE" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "path", + "mode" + ], + "name": "ballerina/log:2.14.0:FileOutputDestination" + } + ] + }, + "description": "" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ] + } + }, + "additionalProperties": false, + "required": [ + "name", + "level" + ], + "name": "ballerina/log:2.14.0:Module" + }, + "description": "" + }, + "keyValues": { + "type": "object", + "properties": { + "msg": {}, + "error": {}, + "stackTrace": {}, + "module": {} + }, + "additionalProperties": false, + "name": "ballerina/log:2.14.0:AnydataKeyValues", + "description": "" + }, + "format": { + "enum": [ + "logfmt", + "json" + ], + "description": "" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ], + "description": "" + }, + "enableSensitiveDataMasking": { + "type": "boolean", + "description": "" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "wso2": { + "type": "object", + "properties": { + "closewon_contract_automation": { + "type": "object", + "properties": { + "salesforceConfig": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "baseUrl": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "clientId", + "clientSecret", + "refreshToken", + "refreshUrl", + "baseUrl" + ], + "name": "wso2/closewon_contract_automation:0.1.0:SalesforceConfig", + "description": "" + }, + "docusignConfig": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "baseUrl": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "accountId", + "clientId", + "clientSecret", + "refreshToken", + "refreshUrl", + "baseUrl" + ], + "name": "wso2/closewon_contract_automation:0.1.0:DocusignConfig", + "description": "" + }, + "templateSettings": { + "type": "object", + "properties": { + "defaultTemplateId": { + "type": "string" + }, + "templateConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "templateId": { + "type": "string" + }, + "productType": { + "type": "string" + }, + "dealType": { + "type": "string" + }, + "expirationDays": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "templateId" + ], + "name": "wso2/closewon_contract_automation:0.1.0:TemplateConfig" + } + } + }, + "additionalProperties": false, + "required": [ + "defaultTemplateId", + "templateConfigs" + ], + "name": "wso2/closewon_contract_automation:0.1.0:TemplateSettings", + "description": "" + }, + "businessRulesConfig": { + "type": "object", + "properties": { + "minimumDealValue": { + "type": "number" + }, + "signerRole": { + "enum": [ + "Executive Sponsor", + "Decision Maker", + "Billing Contact", + "Primary Contact" + ] + }, + "ccRecipients": { + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "email", + "name" + ], + "name": "wso2/closewon_contract_automation:0.1.0:CcRecipient" + } + }, + "fieldMappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "opportunityField": { + "type": "string" + }, + "docusignField": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "opportunityField", + "docusignField" + ], + "name": "wso2/closewon_contract_automation:0.1.0:FieldMapping" + } + }, + "contractSentStage": { + "type": "string" + }, + "expirationReminderDays": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "minimumDealValue", + "signerRole", + "ccRecipients", + "fieldMappings", + "contractSentStage", + "expirationReminderDays" + ], + "name": "wso2/closewon_contract_automation:0.1.0:BusinessRulesConfig", + "description": "" + } + }, + "additionalProperties": false, + "required": [ + "salesforceConfig", + "docusignConfig" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "requiredLevel": 3 +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/diagram.md b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/diagram.md new file mode 100644 index 00000000..b60ac0ac --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/diagram.md @@ -0,0 +1,20 @@ + A(["Start"]):::startNode + B["Listen to Salesforce Opportunity Events"]:::processNode + C{"Is 'Closed Won'?"}:::decisionNode + D["Retrieve Opportunity & Contact Details"]:::processNode + E["Send DocuSign Envelope"]:::processNode + F{"Envelope
Created?"}:::decisionNode + G["Update Stage to 'Contract Sent'"]:::processNode + H["Log Error"]:::processNode + I(["End"]):::endNode + + A --> B + B --> C + C -- Yes --> D + C -- No --> I + D --> E + E --> F + F -- Yes --> G + F -- No --> H + G --> I + H --> I diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/instructions.md b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/instructions.md new file mode 100644 index 00000000..a0bc79f1 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/.choreo/instructions.md @@ -0,0 +1,199 @@ +## What It Does + +- Listens to Salesforce Opportunity change events and validates business criteria (Closed Won stage, minimum deal value) +- Retrieves contact details and selects the appropriate Docusign template based on opportunity type +- Creates and sends Docusign envelope with pre-filled fields from Salesforce +- Updates Salesforce opportunity stage to "Contract Sent" + +
+ +Salesforce Setup Guide + +1. A Salesforce account with API access and Change Data Capture enabled +2. OAuth2 credentials: + - Client ID + - Client Secret + - Refresh Token + - Refresh URL + - Base URL (your Salesforce instance URL) +3. Change Data Capture must be enabled for the **Lead** object in Salesforce Setup + +This integration uses refresh token flow for auth. [Learn how to set up Salesforce OAuth](https://help.salesforce.com/s/articleView?id=xcloud.create_a_local_external_client_app.htm&type=5). + +
+ +
+Docusign Setup Guide + +1. A Docusign account (demo or production) +2. Contract templates created with named fields, note down template IDs and field names for mapping +3. OAuth2 credentials: + - Client ID + - Client Secret + - Refresh Token + - Refresh URL +4. Scopes Required: + - `signature` + - `impersonation` + +This integration uses OAuth2 authentication with refresh token flow. [Learn how to get Docusign credentials](https://developers.docusign.com/platform/auth/). + +
+ +
+ +Configuration Parameters + +### Salesforce Credentials + +- `salesforceUsername` - Your Salesforce username for listener authentication +- `salesforcePassword` - Your Salesforce password with security token appended +- `salesforceClientId` - Your Salesforce OAuth2 client ID +- `salesforceClientSecret` - Your Salesforce OAuth2 client secret +- `salesforceRefreshToken` - Your Salesforce OAuth2 refresh token +- `salesforceRefreshUrl` - Salesforce OAuth2 token endpoint (default: `https://login.salesforce.com/services/oauth2/token`) +- `salesforceBaseUrl` - Your Salesforce instance URL (default: `https://login.salesforce.com`) +- `salesforceChannelName` - Change event channel (default: `/data/ChangeEvents`) + +### Docusign Credentials + +- `docusignAccountId` - Your Docusign account ID +- `docusignAccessToken` - Your Docusign OAuth2 access token +- `docusignBaseUrl` - Docusign API base URL (default: `https://demo.docusign.net/restapi` for demo) + +### Template Configuration + +- `defaultTemplateId` - Default Docusign template ID to use (required) +- `templateConfigs` - Array of template configurations for different product/deal types (optional) + - `templateId` - Docusign template ID + - `productType` - Opportunity type to match (optional) + - `dealType` - Deal type to match (optional) + - `expirationDays` - Days until expiration reminder (optional) + +### Business Rules + +- `minimumDealValue` - Minimum opportunity amount to trigger contract (default: `0.0`) + - Only opportunities with amount >= this value will trigger contract dispatch +- `signerRole` - Contact role to use as signer (default: `"Primary Contact"`) + - Options: `"Primary Contact"`, `"Billing Contact"`, `"Decision Maker"`, `"Executive Sponsor"` + - Falls back to primary contact if specified role not found +- `ccRecipients` - Array of CC recipients (optional) + - `email` - Recipient email address + - `name` - Recipient name +- `fieldMappings` - Array of field mappings from Salesforce to Docusign (default mappings provided) + - `opportunityField` - Salesforce Opportunity field name (e.g., "Name", "Amount", "CloseDate") + - `docusignField` - Docusign template field label (e.g., "OpportunityName", "ContractValue") +- `contractSentStage` - Opportunity stage to set after sending contract (default: `"Contract Sent"`) +- `expirationReminderDays` - Default expiration reminder days (default: `3`) + +
+ +
+ +Field Mapping Examples + +### Default Field Mappings + +The integration comes with these default field mappings: + +```toml +[[fieldMappings]] +opportunityField = "Name" +docusignField = "OpportunityName" + +[[fieldMappings]] +opportunityField = "Amount" +docusignField = "ContractValue" + +[[fieldMappings]] +opportunityField = "CloseDate" +docusignField = "CloseDate" +``` + +### Adding Custom Mappings + +You can add additional field mappings to pre-fill more Docusign fields: + +```toml +[[fieldMappings]] +opportunityField = "Id" +docusignField = "OpportunityId" + +[[fieldMappings]] +opportunityField = "Type" +docusignField = "DealType" + +[[fieldMappings]] +opportunityField = "AccountId" +docusignField = "AccountNumber" +``` + +**Important**: Ensure the `docusignField` values match the exact field labels in your Docusign template. + +
+ +
+ +Template Configuration Examples + +### Single Template Setup + +If you use one template for all contracts: + +```toml +defaultTemplateId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +``` + +### Multiple Templates by Product Type + +Configure different templates for different opportunity types: + +```toml +defaultTemplateId = "default-template-id" + +[[templateConfigs]] +templateId = "enterprise-contract-template-id" +productType = "Enterprise" +expirationDays = 7 + +[[templateConfigs]] +templateId = "professional-contract-template-id" +productType = "Professional" +expirationDays = 5 + +[[templateConfigs]] +templateId = "startup-contract-template-id" +productType = "Startup" +dealType = "New Business" +expirationDays = 3 +``` + +The integration will match the opportunity's `Type` field to select the appropriate template. + +
+ +
+ +CC Recipients Configuration + +### Adding CC Recipients + +Configure multiple people to receive copies of the contract: + +```toml +[[ccRecipients]] +email = "legal@yourcompany.com" +name = "Legal Team" + +[[ccRecipients]] +email = "sales-ops@yourcompany.com" +name = "Sales Operations" + +[[ccRecipients]] +email = "finance@yourcompany.com" +name = "Finance Department" +``` + +All CC recipients will receive a copy of the contract when it's sent to the signer. + +
diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/.gitignore b/ballerina-integrator/salesforce-closewon-contract-automation/.gitignore new file mode 100644 index 00000000..2d54267f --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/.gitignore @@ -0,0 +1,11 @@ +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/Ballerina.toml b/ballerina-integrator/salesforce-closewon-contract-automation/Ballerina.toml new file mode 100644 index 00000000..728cb821 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/Ballerina.toml @@ -0,0 +1,9 @@ +[package] +org = "wso2" +name = "closewon_contract_automation" +version = "0.1.0" +distribution = "2201.13.1" +title = "CloseWon Contract Automation" + +[build-options] +sticky = true diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/Dependencies.toml b/ballerina-integrator/salesforce-closewon-contract-automation/Dependencies.toml new file mode 100644 index 00000000..d6ffdbb8 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/Dependencies.toml @@ -0,0 +1,398 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.13.1" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.9.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "cloud" +version = "4.0.0" +modules = [ + {org = "ballerina", packageName = "cloud", moduleName = "cloud"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "data.jsondata" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.11.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.13.8" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.boolean" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.float" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.xml" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.11.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerinax" +name = "client.config" +version = "1.0.1" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "oauth2"} +] +modules = [ + {org = "ballerinax", packageName = "client.config", moduleName = "client.config"} +] + +[[package]] +org = "ballerinax" +name = "docusign.dsesign" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerinax", packageName = "docusign.dsesign", moduleName = "docusign.dsesign"} +] + +[[package]] +org = "ballerinax" +name = "salesforce" +version = "8.4.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.boolean"}, + {org = "ballerina", name = "lang.float"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.xml"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "client.config"} +] +modules = [ + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.apex"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.bulk"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.bulkv2"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.soap"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.types"}, + {org = "ballerinax", packageName = "salesforce", moduleName = "salesforce.utils"} +] + +[[package]] +org = "wso2" +name = "closewon_contract_automation" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "cloud"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "client.config"}, + {org = "ballerinax", name = "docusign.dsesign"}, + {org = "ballerinax", name = "salesforce"} +] +modules = [ + {org = "wso2", packageName = "closewon_contract_automation", moduleName = "closewon_contract_automation"} +] + diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/README.md b/ballerina-integrator/salesforce-closewon-contract-automation/README.md new file mode 100644 index 00000000..53e7985e --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/README.md @@ -0,0 +1,198 @@ +# Salesforce to Docusign Contract Automation + +## Description + +This integration automatically sends Docusign contracts when Salesforce opportunities are marked as "Closed Won". It listens to Salesforce change events in real-time and triggers contract creation based on configurable business rules. + +> **Note**: This integration uses OAuth2 authentication for the Salesforce event listener, which provides a more secure and modern authentication approach. + +### What It Does + +- Listens to Salesforce Opportunity change events and validates they meet business criteria (Closed Won stage, minimum deal value) +- Retrieves contact details based on configured signer role and selects the appropriate Docusign template +- Creates and sends Docusign envelope with pre-filled fields, configured signers, and CC recipients +- Logs contract status related to the Salesforce opportunity without changing the opportunity stage + +## Prerequisites + +Before running this integration, you need: + +### Salesforce Setup + +1. A Salesforce account with API access +2. **Change Data Capture** enabled for the Opportunity object +3. OAuth2 credentials: + - Client ID + - Client Secret + - Refresh Token + - Refresh URL + - Base URL (your Salesforce instance URL) + +This integration uses OAuth2 refresh token authentication for both API calls and the event listener. [Learn how to set up Salesforce OAuth](https://help.salesforce.com/s/articleView?id=xcloud.create_a_local_external_client_app.htm&type=5). + +### Docusign Setup + +1. A Docusign account (demo or production) +2. Contract templates created with named fields +3. OAuth2 credentials: + - Account ID + - Client ID + - Client Secret + - Refresh Token + - Refresh URL +4. Scopes Required: + - `signature` + - `impersonation` + +This integration uses the official `ballerinax/docusign.dsesign` connector with OAuth2 refresh token authentication for automatic token renewal. [Learn how to get Docusign credentials](https://developers.docusign.com/platform/auth/). + +## Configuration + +Configurations are organized by vendor-specific records for better structure and maintainability. + +### Salesforce Configuration (`salesforceConfig`) + +Record type: `SalesforceConfig` + +- `clientId` - Your Salesforce OAuth2 client ID +- `clientSecret` - Your Salesforce OAuth2 client secret +- `refreshToken` - Your Salesforce OAuth2 refresh token +- `refreshUrl` - Salesforce OAuth2 token endpoint (default: `https://login.salesforce.com/services/oauth2/token`) +- `baseUrl` - Your Salesforce instance URL (default: `https://login.salesforce.com`) + +**Important - Channel Configuration**: +- The service in `main.bal` is configured to listen to `/data/ChangeEvents` by default (captures all object changes) +- To listen to a different channel, update the service declaration in `main.bal`: + - For Opportunity-only events: `service "/data/OpportunityChangeEvent" on salesforceListener` + - For custom channels: `service "/data/YourCustomChannel" on salesforceListener` +- The channel must be enabled in your Salesforce org's Change Data Capture settings + +### Docusign Configuration (`docusignConfig`) + +Record type: `DocusignConfig` + +- `accountId` - Your Docusign account ID +- `clientId` - Your Docusign OAuth2 client ID (Integration Key) +- `clientSecret` - Your Docusign OAuth2 client secret +- `refreshToken` - Your Docusign OAuth2 refresh token +- `refreshUrl` - Docusign OAuth2 token endpoint (default: `https://account-d.docusign.com/oauth/token` for demo, `https://account.docusign.com/oauth/token` for production) +- `baseUrl` - Docusign API base URL (default: `https://demo.docusign.net/restapi` for demo, `https://na1.docusign.net/restapi` for production) + +### Template Configuration (`templateSettings`) + +Record type: `TemplateSettings` + +- `defaultTemplateId` - **REQUIRED** - Default Docusign template ID to use (e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890") + - You can find your template ID in Docusign by going to Templates > Select your template > The ID is in the URL + - Example URL: `https://demo.docusign.net/templates/details/a1b2c3d4-e5f6-7890-abcd-ef1234567890` +- `templateConfigs` - Array of template configurations for different product/deal types (optional) + - `templateId` - Docusign template ID for this specific configuration + - `productType` - Opportunity type to match (optional) + - `dealType` - Deal type to match (optional) + - `expirationDays` - Days until expiration reminder (optional) + +**Example Configuration:** +```toml +[templateSettings] +defaultTemplateId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + +[[templateSettings.templateConfigs]] +templateId = "template-for-enterprise" +productType = "Enterprise" +expirationDays = 7 + +[[templateSettings.templateConfigs]] +templateId = "template-for-standard" +productType = "Standard" +expirationDays = 3 +``` + +### Business Rules Configuration (`businessRulesConfig`) + +Record type: `BusinessRulesConfig` + +- `minimumDealValue` - Minimum opportunity amount to trigger contract (default: 0.0) +- `signerRole` - Contact role to use as signer (options: "Primary Contact", "Billing Contact", "Decision Maker", "Executive Sponsor") +- `ccRecipients` - Array of CC recipients + - `email` - Recipient email address + - `name` - Recipient name +- `fieldMappings` - Array of field mappings from Salesforce to Docusign + - `opportunityField` - Salesforce Opportunity field name + - `docusignField` - Docusign template field label +- `contractSentStage` - Opportunity stage to set after sending contract (default: "Contract Sent") +- `expirationReminderDays` - Default expiration reminder days (default: 3) + +## Deploying on **WSO2 Integration Platform** + +1. Sign in to your WSO2 Integration Platform account. +2. Create a new Integration to import this repository. +3. Select the **Technology** as `WSO2 Integrator: BI`. +4. Choose the **Integration** Type as `Event Handler` and click **Create**. +5. Once the build is successful, click **Configure to Continue** and set up the required environment variables: + +### Required Environment Variables + +**Salesforce Configuration:** +- `salesforceConfig.clientId` - Your Salesforce OAuth2 client ID +- `salesforceConfig.clientSecret` - Your Salesforce OAuth2 client secret +- `salesforceConfig.refreshToken` - Your Salesforce OAuth2 refresh token +- `salesforceConfig.refreshUrl` - Default: `https://login.salesforce.com/services/oauth2/token` +- `salesforceConfig.baseUrl` - Your Salesforce instance URL + +**Docusign Configuration:** +- `docusignConfig.accountId` - Your Docusign account ID +- `docusignConfig.clientId` - Your Docusign OAuth2 client ID (Integration Key) +- `docusignConfig.clientSecret` - Your Docusign OAuth2 client secret +- `docusignConfig.refreshToken` - Your Docusign OAuth2 refresh token +- `docusignConfig.refreshUrl` - Default: `https://account-d.docusign.com/oauth/token` (demo) or `https://account.docusign.com/oauth/token` (production) +- `docusignConfig.baseUrl` - Default: `https://demo.docusign.net/restapi` (demo) or `https://na1.docusign.net/restapi` (production) + +**Template Configuration:** +- `templateSettings.defaultTemplateId` - **REQUIRED** - Your Docusign template ID (find it in Docusign Templates section) + +**Business Rules (Optional):** +- `businessRulesConfig.minimumDealValue` - Minimum opportunity amount (default: 0.0) +- `businessRulesConfig.signerRole` - Contact role for signer (default: "Primary Contact") +- `businessRulesConfig.contractSentStage` - Stage name after sending (default: "Contract Sent") + +6. Click **Deploy** to deploy the integration. +7. Once tested, you may promote the integration to production. Make sure to set the relevant environment variables in the production environment as well. + +## Troubleshooting + +### Common Issues + +1. **Authentication Errors**: + - Verify Salesforce OAuth credentials (client ID, client secret, refresh token) + - Check OAuth token validity and refresh token + - Ensure Connected App permissions are correct (API, Refresh Token, Access and manage your data) + - Verify Docusign OAuth credentials (client ID, client secret, refresh token) + - Ensure Docusign refresh URL matches your environment (demo vs production) + +2. **Event Not Received**: + - Verify Change Data Capture is enabled for Opportunity object in Salesforce + - Check channel name configuration matches Salesforce setup + - Review Salesforce event monitoring logs + - Verify OAuth credentials have proper permissions for event streaming + +3. **Docusign Errors**: + - **"Template ID is empty"**: Configure `defaultTemplateId` in `templateSettings` configuration + - **"Bad Request"**: Common causes: + - Template ID doesn't exist in your Docusign account + - Template role name mismatch (template must have a role named "Signer") + - Field labels in template don't match configured field mappings + - Invalid account ID + - Verify template ID exists in your Docusign account (Templates > Select template > Check URL) + - Check field names in template match configured field mappings + - Ensure access token has required scopes (signature, impersonation) + - Verify account ID is correct + +4. **Contact Not Found**: + - Ensure opportunity has contacts with the configured role + - Check OpportunityContactRole records in Salesforce + - Verify at least one contact is marked as primary + +5. **Validation Errors**: + - Check opportunity has required fields (Name, Amount) + - Verify contact has valid email address + - Ensure opportunity amount meets minimum threshold diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/agents.bal b/ballerina-integrator/salesforce-closewon-contract-automation/agents.bal new file mode 100644 index 00000000..14d47927 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/agents.bal @@ -0,0 +1,36 @@ +// AI Agent integrations for contract automation +// This file can be extended to include AI-powered features such as: +// - Contract review and analysis +// - Automated field extraction from opportunity data +// - Intelligent template selection based on deal characteristics +// - Risk assessment and approval routing + +import ballerina/log; + +// Placeholder for future AI agent integration +// Example: Analyze opportunity to determine contract complexity +public function analyzeOpportunityComplexity(Opportunity opportunity) returns string { + decimal? amount = opportunity.Amount; + + if amount is () { + return "standard"; + } + + if amount > 100000d { + return "complex"; + } else if amount > 50000d { + return "moderate"; + } + + return "standard"; +} + +// Placeholder for AI-powered template selection +public function recommendTemplate(Opportunity opportunity) returns string? { + string complexity = analyzeOpportunityComplexity(opportunity); + + log:printInfo(string `Opportunity complexity: ${complexity}`); + + // Future: Use AI to recommend best template based on historical data + return (); +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/automation.bal b/ballerina-integrator/salesforce-closewon-contract-automation/automation.bal new file mode 100644 index 00000000..8c6e8794 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/automation.bal @@ -0,0 +1,286 @@ +import ballerina/log; +import ballerinax/docusign.dsesign; + +// Create and send DocuSign envelope using the connector +function createAndSendEnvelope(Opportunity opportunity, Contact signer, TemplateConfig templateConfig) returns string|error { + // Build signer name + string signerName = buildSignerName(signer); + + // Build pre-fill fields + record {string name; string value;}[] templateFields = buildTemplateFields(opportunity); + + // Get signer email + string? signerEmail = signer.Email; + if signerEmail is () { + return error("Contact email is required"); + } + + // Get opportunity name + string? opportunityName = opportunity.Name; + string emailSubject = opportunityName is string ? string `Contract for ${opportunityName}` : "Contract"; + + log:printInfo(string `Creating envelope with template: ${templateConfig.templateId}`); + log:printInfo(string `Signer: ${signerName} (${signerEmail})`); + log:printInfo(string `Email subject: ${emailSubject}`); + + // Build template roles + dsesign:TemplateRole[] templateRoles = []; + + // Add signer + dsesign:TemplateRole signerRole = { + email: signerEmail, + name: signerName, + roleName: "Signer", + routingOrder: "1" + }; + + // Add tabs for pre-filled fields + if templateFields.length() > 0 { + log:printInfo(string `Adding ${templateFields.length()} pre-filled fields`); + dsesign:Text[] textTabs = []; + + foreach record {string name; string value;} templateField in templateFields { + log:printInfo(string `Field: ${templateField.name} = ${templateField.value}`); + dsesign:Text textTab = { + tabLabel: templateField.name, + value: templateField.value + }; + textTabs.push(textTab); + } + + dsesign:Tabs tabs = { + textTabs: textTabs + }; + + signerRole.tabs = tabs; + } + + templateRoles.push(signerRole); + + // Add CC recipients if configured + if businessRulesConfig.ccRecipients.length() > 0 { + log:printInfo(string `Adding ${businessRulesConfig.ccRecipients.length()} CC recipients`); + int ccOrder = 2; + foreach CcRecipient ccRecipient in businessRulesConfig.ccRecipients { + log:printInfo(string `CC: ${ccRecipient.name} (${ccRecipient.email})`); + dsesign:TemplateRole ccRole = { + email: ccRecipient.email, + name: ccRecipient.name, + roleName: "CC", + routingOrder: ccOrder.toString() + }; + templateRoles.push(ccRole); + ccOrder = ccOrder + 1; + } + } + + // Create envelope definition for DocuSign API + // Note: When using templates, ensure the template has documents attached in DocuSign + // We'll create the envelope in "sent" status to immediately send it + dsesign:EnvelopeDefinition envelopeDefinition = { + emailSubject: emailSubject, + templateId: templateConfig.templateId, + templateRoles: templateRoles, + status: "sent" + }; + + // Create and send envelope using DocuSign client + log:printInfo("Sending envelope creation request to DocuSign..."); + log:printInfo(string `Template ID: ${templateConfig.templateId}`); + log:printInfo(string `Email Subject: ${emailSubject}`); + log:printInfo(string `Number of template roles: ${templateRoles.length()}`); + + dsesign:EnvelopeSummary|error envelopeResult = docusignClient->/accounts/[docusignConfig.accountId]/envelopes.post(envelopeDefinition); + + if envelopeResult is error { + log:printError(string `DocuSign API error: ${envelopeResult.message()}`); + log:printError(string `Error detail: ${envelopeResult.toString()}`); + + // Provide helpful error messages + string errorMsg = envelopeResult.message(); + if errorMsg.includes("ENVELOPE_IS_INCOMPLETE") { + log:printError("TROUBLESHOOTING: The template must have documents attached in DocuSign."); + log:printError("Please verify in DocuSign: Templates > Select your template > Ensure documents are added"); + log:printError(string `Template ID being used: ${templateConfig.templateId}`); + } + + return error(string `Failed to create DocuSign envelope: ${envelopeResult.message()}`); + } + + dsesign:EnvelopeSummary envelopeSummary = envelopeResult; + string? envelopeId = envelopeSummary.envelopeId; + + if envelopeId is () { + return error("Failed to create envelope: No envelope ID returned"); + } + + log:printInfo(string `DocuSign envelope created and sent successfully: ${envelopeId}`); + return envelopeId; +} + +// Build template fields from opportunity data +function buildTemplateFields(Opportunity opportunity) returns record {string name; string value;}[] { + record {string name; string value;}[] fields = []; + + foreach FieldMapping mapping in businessRulesConfig.fieldMappings { + string opportunityField = mapping.opportunityField; + string docusignField = mapping.docusignField; + + // Get field value from opportunity + string fieldValue = getOpportunityFieldValue(opportunity, opportunityField); + + if fieldValue != "" { + fields.push({ + name: docusignField, + value: fieldValue + }); + } + } + + return fields; +} + +// Build signer name from contact +function buildSignerName(Contact contact) returns string { + string? firstName = contact.FirstName; + string? lastName = contact.LastName; + + if firstName is string && lastName is string { + return string `${firstName} ${lastName}`; + } + + if lastName is string { + return lastName; + } + + if firstName is string { + return firstName; + } + + return "Signer"; +} + +// Validate DocuSign configuration +function validateDocusignConfig() returns error? { + if docusignConfig.accountId.trim() == "" { + return error("DocuSign account ID is not configured"); + } + + if docusignConfig.clientId.trim() == "" { + return error("DocuSign client ID is not configured"); + } + + if docusignConfig.clientSecret.trim() == "" { + return error("DocuSign client secret is not configured"); + } + + if docusignConfig.refreshToken.trim() == "" { + return error("DocuSign refresh token is not configured"); + } + + // Log reminder about template requirements + log:printInfo("REMINDER: Ensure your DocuSign template has documents attached and a 'Signer' role defined"); +} + +// Process opportunity for contract dispatch +public function processOpportunityForContract(string opportunityId) returns error? { + log:printInfo(string `Processing opportunity ${opportunityId} for contract dispatch`); + + // Validate DocuSign configuration + error? configValidation = validateDocusignConfig(); + if configValidation is error { + log:printError(string `DocuSign configuration error: ${configValidation.message()}`); + return configValidation; + } + + // Get opportunity details + Opportunity|error opportunityResult = getOpportunity(opportunityId); + if opportunityResult is error { + log:printError(string `Failed to get opportunity ${opportunityId}: ${opportunityResult.message()}`); + return opportunityResult; + } + Opportunity opportunity = opportunityResult; + string? opportunityName = opportunity.Name; + string? stageName = opportunity.StageName; + string nameStr = opportunityName is string ? opportunityName : "Unknown"; + string stageStr = stageName is string ? stageName : "Unknown"; + log:printInfo(string `Retrieved opportunity: ${nameStr}, Stage: ${stageStr}`); + + // Validate opportunity data + error? validationResult = validateOpportunityData(opportunity); + if validationResult is error { + log:printError(string `Opportunity validation failed: ${validationResult.message()}`); + return validationResult; + } + + // Check if opportunity meets dispatch criteria + if !meetsDispatchCriteria(opportunity) { + log:printInfo(string `Opportunity ${opportunityId} does not meet dispatch criteria`); + return; + } + + // Get signer contact based on configured role + Contact|error signerResult = getSignerContact(opportunityId); + if signerResult is error { + log:printError(string `Failed to get signer contact: ${signerResult.message()}`); + return signerResult; + } + Contact signer = signerResult; + + // Validate contact data + error? contactValidationResult = validateContactData(signer); + if contactValidationResult is error { + log:printError(string `Contact validation failed: ${contactValidationResult.message()}`); + return contactValidationResult; + } + + // Select appropriate template + TemplateConfig templateConfig = selectTemplate(opportunity); + log:printInfo(string `Selected template: ${templateConfig.templateId}`); + + // Validate template ID + if templateConfig.templateId.trim() == "" { + error templateError = error("Template ID is empty. Please configure 'defaultTemplateId' in templateSettings configuration. You can find your template ID in Docusign: Templates > Select template > Copy ID from URL."); + log:printError(templateError.message()); + return templateError; + } + + log:printInfo("IMPORTANT: Before sending, verify your DocuSign template:"); + log:printInfo(" 1. Has at least one document attached (PDF/Word/etc.)"); + log:printInfo(" 2. Has a recipient role named 'Signer' (case-sensitive)"); + log:printInfo(" 3. Is in 'Active' status"); + log:printInfo(string ` Template ID: ${templateConfig.templateId}`); + + // Create and send DocuSign envelope + string|error envelopeResult = createAndSendEnvelope(opportunity, signer, templateConfig); + if envelopeResult is error { + log:printError(string `Failed to create DocuSign envelope: ${envelopeResult.message()}`); + return envelopeResult; + } + string envelopeId = envelopeResult; + + log:printInfo(string `DocuSign envelope ${envelopeId} sent for opportunity ${opportunityId}`); + + // Update opportunity stage to "Contract Sent" + error? updateResult = updateOpportunityStage(opportunityId, businessRulesConfig.contractSentStage); + if updateResult is error { + log:printWarn(string `Failed to update opportunity stage: ${updateResult.message()}`); + } + + log:printInfo(string `Successfully processed opportunity ${opportunityId}`); +} + +// Get signer contact based on configured role +function getSignerContact(string opportunityId) returns Contact|error { + // Try to get contact by configured role + Contact|error contactResult = getContactByRole(opportunityId, businessRulesConfig.signerRole); + + if contactResult is Contact { + return contactResult; + } + + // Fallback to primary contact + log:printWarn(string `Could not find contact with role ${businessRulesConfig.signerRole}, falling back to primary contact`); + Contact primaryContact = check getPrimaryContact(opportunityId); + return primaryContact; +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/config.bal b/ballerina-integrator/salesforce-closewon-contract-automation/config.bal new file mode 100644 index 00000000..dac90b06 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/config.bal @@ -0,0 +1,25 @@ +// Salesforce Configuration +configurable SalesforceConfig salesforceConfig = ?; + +// DocuSign Configuration +configurable DocusignConfig docusignConfig = ?; + +// Template Configuration +configurable TemplateSettings templateSettings = { + defaultTemplateId: "", + templateConfigs: [] +}; + +// Business Rules Configuration +configurable BusinessRulesConfig businessRulesConfig = { + minimumDealValue: 0.0, + signerRole: PRIMARY_CONTACT, + ccRecipients: [], + fieldMappings: [ + {opportunityField: "Name", docusignField: "OpportunityName"}, + {opportunityField: "Amount", docusignField: "ContractValue"}, + {opportunityField: "CloseDate", docusignField: "CloseDate"} + ], + contractSentStage: "Contract Sent", + expirationReminderDays: 3 +}; diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/connections.bal b/ballerina-integrator/salesforce-closewon-contract-automation/connections.bal new file mode 100644 index 00000000..19d7ce4e --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/connections.bal @@ -0,0 +1,43 @@ +import ballerinax/salesforce; +import ballerinax/'client.config as clientConfig; +import ballerinax/docusign.dsesign; + +// Salesforce OAuth configuration for client +final clientConfig:OAuth2RefreshTokenGrantConfig salesforceOAuthConfig = { + clientId: salesforceConfig.clientId, + clientSecret: salesforceConfig.clientSecret, + refreshToken: salesforceConfig.refreshToken, + refreshUrl: salesforceConfig.refreshUrl +}; + +// Salesforce Client Configuration +final salesforce:Client salesforceClient = check new ({ + baseUrl: salesforceConfig.baseUrl, + auth: salesforceOAuthConfig +}); + +// DocuSign Client Configuration +final dsesign:Client docusignClient = check new ( + config = { + auth: { + clientId: docusignConfig.clientId, + clientSecret: docusignConfig.clientSecret, + refreshToken: docusignConfig.refreshToken, + refreshUrl: docusignConfig.refreshUrl + } + }, + serviceUrl = docusignConfig.baseUrl +); + +// Salesforce Listener Configuration +// The listener now supports OAuth2 authentication for REST-based event streaming +// Using RestBasedListenerConfig with OAuth2RefreshTokenGrantConfig +listener salesforce:Listener salesforceListener = new ({ + baseUrl: salesforceConfig.baseUrl, + auth: { + clientId: salesforceConfig.clientId, + clientSecret: salesforceConfig.clientSecret, + refreshToken: salesforceConfig.refreshToken, + refreshUrl: salesforceConfig.refreshUrl + } +}); diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/data_mappings.bal b/ballerina-integrator/salesforce-closewon-contract-automation/data_mappings.bal new file mode 100644 index 00000000..eb6030b3 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/data_mappings.bal @@ -0,0 +1,92 @@ +// Data mapping utilities for transforming Salesforce data to DocuSign format + +import ballerina/log; + +// Map Salesforce opportunity to DocuSign envelope subject +public function mapEnvelopeSubject(Opportunity opportunity) returns string { + string? opportunityName = opportunity.Name; + if opportunityName is string { + return string `Contract for ${opportunityName}`; + } + return "Contract"; +} + +// Map Salesforce contact to DocuSign signer +public function mapContactToSigner(Contact contact, string roleName) returns SignerInfo|error { + string signerName = buildContactName(contact); + + string? contactEmail = contact.Email; + if contactEmail is () { + return error("Contact email is required"); + } + + return { + email: contactEmail, + name: signerName, + roleName: roleName, + routingOrder: 1 + }; +} + +// Build full contact name +function buildContactName(Contact contact) returns string { + string? firstName = contact.FirstName; + string? lastName = contact.LastName; + + if firstName is string && lastName is string { + return string `${firstName} ${lastName}`; + } + + if lastName is string { + return lastName; + } + + if firstName is string { + return firstName; + } + + return "Contact"; +} + +// Validate opportunity data before processing +public function validateOpportunityData(Opportunity opportunity) returns error? { + // Validate required fields + string? opportunityName = opportunity.Name; + if opportunityName is () || opportunityName.trim() == "" { + return error("Opportunity name is required"); + } + + decimal? amount = opportunity.Amount; + if amount is () { + return error("Opportunity amount is required"); + } + + if amount < 0d { + return error("Opportunity amount must be positive"); + } + + log:printInfo(string `Validated opportunity ${opportunity.Id}: ${opportunityName}`); +} + +// Validate contact data before processing +public function validateContactData(Contact contact) returns error? { + // Validate email + string? contactEmail = contact.Email; + if contactEmail is () || contactEmail.trim() == "" { + return error("Contact email is required"); + } + + // Basic email validation + if !contactEmail.includes("@") { + return error(string `Invalid email format: ${contactEmail}`); + } + + // Validate name + string? lastName = contact.LastName; + string? firstName = contact.FirstName; + if (lastName is () || lastName.trim() == "") && (firstName is () || firstName.trim() == "") { + return error("Contact must have at least first name or last name"); + } + + log:printInfo(string `Validated contact ${contact.Id}: ${contactEmail}`); +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/functions.bal b/ballerina-integrator/salesforce-closewon-contract-automation/functions.bal new file mode 100644 index 00000000..8287a8a5 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/functions.bal @@ -0,0 +1,189 @@ +import ballerina/log; + +// Get opportunity details from Salesforce +function getOpportunity(string opportunityId) returns Opportunity|error { + log:printInfo(string `Fetching opportunity details for ID: ${opportunityId}`); + Opportunity|error opportunityResult = salesforceClient->getById("Opportunity", opportunityId, Opportunity); + if opportunityResult is error { + log:printError(string `Salesforce API error: ${opportunityResult.message()}`); + return opportunityResult; + } + return opportunityResult; +} + +// Get contact by role from opportunity +function getContactByRole(string opportunityId, SignerRole role) returns Contact|error { + // Query for OpportunityContactRole + string soqlQuery = string `SELECT ContactId, Role FROM OpportunityContactRole + WHERE OpportunityId = '${opportunityId}' + AND Role = '${role}' + LIMIT 1`; + + stream roleStream = check salesforceClient->query(soqlQuery); + + record {}[] roles = check from record {} roleRecord in roleStream + select roleRecord; + + if roles.length() == 0 { + return error(string `No contact found with role: ${role}`); + } + + record {} contactRole = roles[0]; + json contactRoleJson = contactRole.toJson(); + + if contactRoleJson !is map { + return error("Invalid contact role data structure"); + } + + map contactRoleMap = contactRoleJson; + json contactIdJson = contactRoleMap["ContactId"]; + + if contactIdJson is () { + return error("ContactId not found in query result"); + } + + string contactId = contactIdJson.toString(); + + // Get contact details + Contact contact = check salesforceClient->getById("Contact", contactId, Contact); + return contact; +} + +// Get primary contact from opportunity +function getPrimaryContact(string opportunityId) returns Contact|error { + // Query for primary contact role + string soqlQuery = string `SELECT ContactId FROM OpportunityContactRole + WHERE OpportunityId = '${opportunityId}' + AND IsPrimary = true + LIMIT 1`; + + stream roleStream = check salesforceClient->query(soqlQuery); + + record {}[] roles = check from record {} roleRecord in roleStream + select roleRecord; + + if roles.length() == 0 { + return error("No primary contact found for opportunity"); + } + + record {} contactRole = roles[0]; + json contactRoleJson = contactRole.toJson(); + + if contactRoleJson !is map { + return error("Invalid contact role data structure"); + } + + map contactRoleMap = contactRoleJson; + json contactIdJson = contactRoleMap["ContactId"]; + + if contactIdJson is () { + return error("ContactId not found in query result"); + } + + string contactId = contactIdJson.toString(); + + // Get contact details + Contact contact = check salesforceClient->getById("Contact", contactId, Contact); + return contact; +} + +// Select appropriate template based on opportunity +function selectTemplate(Opportunity opportunity) returns TemplateConfig { + // Check if there are configured templates + foreach TemplateConfig templateConfig in templateSettings.templateConfigs { + string? productType = templateConfig.productType; + string? dealType = templateConfig.dealType; + string? oppType = opportunity.Type; + + // Match by product type or deal type + if productType is string && oppType is string && oppType == productType { + return templateConfig; + } + + if dealType is string && oppType is string && oppType == dealType { + return templateConfig; + } + } + + // Return default template + return { + templateId: templateSettings.defaultTemplateId, + expirationDays: businessRulesConfig.expirationReminderDays + }; +} + +// Note: buildTemplateFields function moved to automation.bal + +// Get opportunity field value by field name +function getOpportunityFieldValue(Opportunity opportunity, string fieldName) returns string { + match fieldName { + "Name" => { + string? opportunityName = opportunity.Name; + if opportunityName is string { + return opportunityName; + } + } + "Amount" => { + decimal? amount = opportunity.Amount; + if amount is decimal { + return amount.toString(); + } + } + "CloseDate" => { + string? closeDate = opportunity.CloseDate; + if closeDate is string { + return closeDate; + } + } + "Id" => { + return opportunity.Id; + } + "StageName" => { + string? stageName = opportunity.StageName; + if stageName is string { + return stageName; + } + } + "Type" => { + string? oppType = opportunity.Type; + if oppType is string { + return oppType; + } + } + } + + return ""; +} + +// Update Salesforce opportunity stage +function updateOpportunityStage(string opportunityId, string stageName) returns error? { + // NOTE: This function does not currently perform a real Salesforce update. + // To enable contract status tracking and prevent duplicate envelope sends, + // wire this to the Salesforce REST API (PATCH /sobjects/Opportunity/{Id}) + // to set the StageName field appropriately. + log:printError(string `Opportunity stage update for ${opportunityId} to ${stageName} is not implemented`); + return error("Opportunity stage update is not implemented; integrate with Salesforce REST API before using this function in production flows"); +} + +// Check if opportunity meets criteria +function meetsDispatchCriteria(Opportunity opportunity) returns boolean { + // Check if stage is Closed Won + string? stageName = opportunity.StageName; + if stageName is () || stageName != "Closed Won" { + return false; + } + + // Check minimum deal value + decimal? amount = opportunity.Amount; + if amount is () { + log:printWarn(string `Opportunity ${opportunity.Id} has no amount specified`); + return false; + } + + if amount < businessRulesConfig.minimumDealValue { + log:printInfo(string `Opportunity ${opportunity.Id} amount ${amount} is below minimum threshold ${businessRulesConfig.minimumDealValue}`); + return false; + } + + return true; +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/main.bal b/ballerina-integrator/salesforce-closewon-contract-automation/main.bal new file mode 100644 index 00000000..546295c0 --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/main.bal @@ -0,0 +1,80 @@ +import ballerinax/salesforce; +import ballerina/log; + +// Salesforce Listener Service for Change Events +// The service name MUST be the exact Salesforce channel path you want to listen to +// Options: +// - "/data/ChangeEvents" - All object changes (default) +// - "/data/OpportunityChangeEvent" - Opportunity changes only +// - "/data/AccountChangeEvent" - Account changes only +// - Custom channels as configured in Salesforce Change Data Capture +// Note: The channel must be enabled in Salesforce Setup > Change Data Capture +service "/data/ChangeEvents" on salesforceListener { + + // Handle opportunity creation events + remote function onCreate(salesforce:EventData eventData) returns error? { + log:printInfo("Received onCreate event from Salesforce"); + // Contract dispatch is only for Closed Won opportunities + // onCreate events are typically not Closed Won, so we skip + } + + // Handle opportunity update events + remote function onUpdate(salesforce:EventData eventData) returns error? { + log:printInfo("Received onUpdate event from Salesforce"); + + // Extract opportunity ID from event metadata + // The EventData has a metadata field that contains the ChangeEventMetadata + salesforce:ChangeEventMetadata? metadataValue = eventData.metadata; + + if metadataValue is () { + log:printError("No metadata found in event data"); + return; + } + + // Convert metadata to JSON to access fields + json metadataJson = metadataValue.toJson(); + + if metadataJson !is map { + log:printError("Metadata is not a JSON map"); + return error("Invalid metadata structure"); + } + + map metadataMap = metadataJson; + + // Log the metadata for debugging + log:printInfo(string `Event metadata: ${metadataMap.toJsonString()}`); + + // Get recordId from metadata (singular field) + json recordIdJson = metadataMap["recordId"]; + + if recordIdJson is () { + log:printError("No recordId found in event metadata"); + return; + } + + // Convert to string + string opportunityId = recordIdJson.toString(); + + log:printInfo(string `Processing opportunity ID: ${opportunityId}`); + + // Process opportunity for contract dispatch + error? result = processOpportunityForContract(opportunityId); + + if result is error { + log:printError(string `Error processing opportunity ${opportunityId}: ${result.message()}`); + return result; + } + } + + // Handle opportunity delete events + remote function onDelete(salesforce:EventData eventData) returns error? { + log:printInfo("Received onDelete event from Salesforce"); + // No action needed for delete events + } + + // Handle opportunity restore events + remote function onRestore(salesforce:EventData eventData) returns error? { + log:printInfo("Received onRestore event from Salesforce"); + // No action needed for restore events + } +} diff --git a/ballerina-integrator/salesforce-closewon-contract-automation/types.bal b/ballerina-integrator/salesforce-closewon-contract-automation/types.bal new file mode 100644 index 00000000..5976346d --- /dev/null +++ b/ballerina-integrator/salesforce-closewon-contract-automation/types.bal @@ -0,0 +1,119 @@ +// Salesforce Opportunity record +public type Opportunity record { + string Id; + string? Name; + string? StageName; + decimal? Amount; + string? AccountId; + string? Type; + string? CloseDate; +}; + +// Salesforce Contact record +public type Contact record { + string Id; + string? FirstName; + string? LastName; + string? Email; + string? AccountId; +}; + +// Salesforce OpportunityContactRole record +public type OpportunityContactRole record { + string Id; + string OpportunityId; + string ContactId; + string? Role; + boolean? IsPrimary; +}; + +// Note: DocuSign types are now provided by ballerinax/docusign.dsesign connector +// Custom types for our specific use case + +// Simplified envelope creation request +public type EnvelopeRequest record { + string emailSubject; + string templateId; + SignerInfo[] signers; + CarbonCopy[] carbonCopies?; + record {string name; string value;}[] templateFields?; + int expirationDays?; +}; + +// Signer information +public type SignerInfo record { + string email; + string name; + string roleName; + int routingOrder?; +}; + +// Carbon copy recipient +public type CarbonCopy record { + string email; + string name; + int routingOrder?; +}; + +// Template Configuration +public type TemplateConfig record { + string templateId; + string productType?; + string dealType?; + int expirationDays?; +}; + +// Signer Role Mapping +public enum SignerRole { + PRIMARY_CONTACT = "Primary Contact", + BILLING_CONTACT = "Billing Contact", + DECISION_MAKER = "Decision Maker", + EXECUTIVE_SPONSOR = "Executive Sponsor" +} + +// CC Recipient +public type CcRecipient record { + string email; + string name; +}; + +// Field Mapping Configuration +public type FieldMapping record { + string opportunityField; + string docusignField; +}; + +// Salesforce Configuration Record +public type SalesforceConfig record {| + string clientId; + string clientSecret; + string refreshToken; + string refreshUrl = "https://login.salesforce.com/services/oauth2/token"; + string baseUrl = "https://login.salesforce.com"; +|}; + +// DocuSign Configuration Record +public type DocusignConfig record {| + string accountId; + string clientId; + string clientSecret; + string refreshToken; + string refreshUrl = "https://account-d.docusign.com/oauth/token"; + string baseUrl = "https://demo.docusign.net/restapi"; +|}; + +// Template Settings Record +public type TemplateSettings record {| + string defaultTemplateId; + TemplateConfig[] templateConfigs; +|}; + +// Business Rules Configuration Record +public type BusinessRulesConfig record {| + decimal minimumDealValue; + SignerRole signerRole; + CcRecipient[] ccRecipients; + FieldMapping[] fieldMappings; + string contractSentStage; + int expirationReminderDays; +|};