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;
+|};