Add Salesforce to Stripe Customer Sync Integration - #69
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new Salesforce→Stripe integration module: project manifest, configuration, client connections, types, validation/mapping, sync/delete logic, CDC event listener with handlers, bulk sync utilities, and documentation/diagrams. Changes
Sequence Diagram(s)sequenceDiagram
participant SF as Salesforce
participant CDC as ChangeEventListener
participant Val as Validation
participant Map as Mapping
participant Sync as SyncLogic
participant Stripe as StripeAPI
participant WB as SalesforceWriteback
SF->>CDC: CDC Event (Account/Contact)
CDC->>Val: Validate record
Val-->>CDC: Valid / Invalid
alt Valid
CDC->>Map: Map to Stripe payload
Map->>Sync: Sync (create/update/delete)
alt Create
Sync->>Stripe: Create customer (Idempotency-Key)
Stripe-->>Sync: Customer ID
Sync->>WB: Write back Stripe ID to Salesforce
WB-->>Sync: Acknowledge
else Update/Delete
Sync->>Stripe: Update or Delete customer
Stripe-->>Sync: Acknowledge
end
else Invalid
CDC-->>SF: Log / skip
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
ballerina-integrator/SFtoStripe/bulk_sync.bal (1)
18-27: Consider adding batch progress logging for large datasets.For large Salesforce orgs, the bulk sync could process thousands of records. Consider adding periodic progress logs (e.g., every 100 records) to provide visibility during long-running operations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/bulk_sync.bal` around lines 18 - 27, Add periodic progress logging inside the loop that processes accountStream to report progress every N records (e.g., 100). Update the block around the check from SalesforceAccount ... do { ... } to increment a counter (use existing errorCount and successCount or a new processedCount) and, when processedCount % 100 == 0, call log:printInfo (or appropriate logger) with a message including processedCount, successCount, and errorCount; keep existing per-record error logging and calls to syncAccountToStripe unchanged. Ensure the logging uses clear context (e.g., batch size and record IDs) and does not change syncAccountToStripe semantics.ballerina-integrator/SFtoStripe/data_mappings.bal (1)
45-45: Remove or reduce log level for debug logging.The
log:printInfoon every contact mapping is verbose for production. Consider removing it or usinglog:printDebuginstead.♻️ Proposed fix
- log:printInfo("[mapContactToStripeCustomer] Building name", firstName = firstName, lastName = lastName, fullName = fullName); + log:printDebug("[mapContactToStripeCustomer] Building name", firstName = firstName, lastName = lastName, fullName = fullName);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/data_mappings.bal` at line 45, The info-level logging call log:printInfo("[mapContactToStripeCustomer] Building name", firstName = firstName, lastName = lastName, fullName = fullName) inside the mapContactToStripeCustomer routine is too verbose for production; change it to use debug-level logging (log:printDebug) or remove the statement entirely so mapping doesn't emit info logs for every contact—locate the log:printInfo call in mapContactToStripeCustomer and replace or delete it accordingly.ballerina-integrator/SFtoStripe/README.md (1)
3-13: Markdown headers are missing formatting.The section titles (Description, What it does, Prerequisites, etc.) should use proper markdown heading syntax for better rendering.
📝 Proposed fix for markdown headers
# Salesforce to Stripe Customer Sync Integration -Description +## Description This integration listens for Salesforce Account and Contact creation, update, and deletion events and creates or updates corresponding customers in Stripe. -What it does +## What it does - When an Account or Contact is created in Salesforce, the integration creates a customer in Stripe with the customer detailsApply similar fixes to "Prerequisites", "Salesforce Setup", "Stripe Setup", "Configuration", "Salesforce Configuration", "Stripe Configuration", and "Deploying on Devant" sections.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/README.md` around lines 3 - 13, Convert plain-text section titles into proper Markdown headers: replace "Description" with "# Description", "What it does" with "## What it does", and similarly update other section titles mentioned in the file (e.g., "Prerequisites", "Salesforce Setup", "Stripe Setup", "Configuration", "Salesforce Configuration", "Stripe Configuration", "Deploying on Devant") to appropriate heading levels (use H1 for the main title and H2/H3 for subsections) so the README renders correctly; keep existing content under each title intact and apply consistent heading levels across the document.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ballerina-integrator/SFtoStripe/bulk_sync.bal`:
- Around line 7-9: The SOQL SELECT assigned to soqlQuery in bulk_sync.bal omits
Email__c while mapAccountToStripeCustomer expects it; update the soqlQuery
string to include Email__c in the SELECT clause (e.g., add ", Email__c" among
the selected Account fields) so the mapAccountToStripeCustomer mapping can
populate the Stripe customer email.
In `@ballerina-integrator/SFtoStripe/config.bal`:
- Around line 14-20: The isolated qualifier on functions in validation.bal and
data_mappings.bal that read the configurable variables matchKey,
recordTypeFilter, and accountStatusFilter causes compilation errors; remove the
isolated keyword from those consumer functions (any functions that reference
matchKey, recordTypeFilter, or accountStatusFilter) so they are no longer
declared isolated, keeping their logic unchanged, and update any function
declarations/signatures accordingly to match the non-isolated form.
In `@ballerina-integrator/SFtoStripe/data_mappings.bal`:
- Around line 66-106: The isolated function passesFilters illegally accesses
non-final module-level variables recordTypeFilter and accountStatusFilter; to
fix, either remove the isolated qualifier from passesFilters or make those
variables final (declare recordTypeFilter and accountStatusFilter as final) so
they can be read from an isolated function—update the declaration of the chosen
variables or the function signature accordingly and ensure uses inside
passesFilters remain unchanged.
In `@ballerina-integrator/SFtoStripe/functions.bal`:
- Line 80: The logs are dumping full Stripe customer payloads via
customerPayload.toString(); remove these payload dumps or replace them with a
redacted summary before logging. In the syncContactToStripe logging calls (look
for log:printInfo with the "[syncContactToStripe]" tag and
payload=customerPayload.toString()), either delete the payload argument or
construct a small sanitized object that only includes non-sensitive fields
(e.g., customer ID, status) and log that instead; apply the same change to the
other occurrences noted (the similar log:printInfo calls around the other two
locations). Ensure no full payload.toString() is emitted.
- Around line 145-170: Replace the paginated GET /customers loop that iterates
using startingAfter, stripeClient and stripe:CustomerResourceCustomerList with a
single call to the Stripe Customer Search API: invoke
stripeClient->/customers.search with
query=metadata["salesforce_id"]:"<salesforceId>" to retrieve matching customers,
then iterate result.data to delete any returned customers (use the same delete
call stripeClient->/customers/[c.id].delete()). Remove the while/startingAfter
pagination logic and ensure you handle the possibility of multiple matches and
the search API's potential eventual-consistency behavior in calling code.
In `@ballerina-integrator/SFtoStripe/main.bal`:
- Around line 137-176: The handler currently falls back to using the partial CDC
delta (via data.cloneWithType()) when the SOQL full-record fetch
(salesforceClient->query / queryResult.next()) fails, then calls
syncAccountToStripe(account, true), which can create duplicate Stripe customers;
change the error paths in the onUpdate flow (where you handle queryResult is
error, queryRecord is error, and queryRecord is empty) to stop using the partial
CDC data—log the failure and return or surface an error instead of assigning
cdcAccount to account and calling syncAccountToStripe; update the same logic in
the corresponding block around the other occurrence (lines ~179-218) so
syncAccountToStripe is only called with the full-record Account containing
Stripe_Customer_Id__c.
- Line 46: The log call is emitting raw customer data (account,
data.toString()); replace direct logging with a sanitized representation by
adding and calling a sanitizer (e.g., sanitizeAccount(account) or
sanitizePayload(data)) that redacts PII (names, emails, identifiers) before
passing to log:printError, and update all similar occurrences (the
log:printError at line with account/data.toString() and the other ranges 62-73,
145-173, 187-215, 228-231) to use the sanitizer rather than raw payloads so logs
never contain unredacted customer data.
- Around line 250-260: The onDelete branch currently calls
deleteStripeCustomerBySalesforceId unconditionally; guard those calls with the
delete toggle by checking deleteStripeCustomerOnSalesforceDelete before invoking
deleteStripeCustomerBySalesforceId for both Account and Contact branches (the
blocks that reference entityType, sourceObject, ACCOUNT, CONTACT, BOTH and
recordId). If the toggle is false, skip the delete call and emit a clear log
(e.g., using log:printInfo) indicating the Stripe customer deletion was skipped
for the given recordId; otherwise proceed and keep the existing error logging
for deleteStripeCustomerBySalesforceId.
In `@ballerina-integrator/SFtoStripe/validation.bal`:
- Around line 28-49: The function validateContact is declared isolated but
accesses the mutable configurable matchKey (same issue as validateAccount);
remove the isolated qualifier from validateContact so it no longer enforces
isolation constraints, leaving the function signature as public function
validateContact(...) returns error? and keep the body unchanged; ensure you
update any tests or call sites if they relied on the function being isolated.
- Around line 4-25: The isolated function validateAccount illegally reads the
non-final module-level configurable matchKey, causing an isolation violation;
fix by either removing the isolated qualifier from validateAccount or by
changing its signature to accept matchKey (e.g., add a parameter like matchKey:
MatchKeyType) and use that parameter inside the function, and ensure all call
sites pass the matchKey; reference validateAccount and the module-level variable
matchKey when making the change.
---
Nitpick comments:
In `@ballerina-integrator/SFtoStripe/bulk_sync.bal`:
- Around line 18-27: Add periodic progress logging inside the loop that
processes accountStream to report progress every N records (e.g., 100). Update
the block around the check from SalesforceAccount ... do { ... } to increment a
counter (use existing errorCount and successCount or a new processedCount) and,
when processedCount % 100 == 0, call log:printInfo (or appropriate logger) with
a message including processedCount, successCount, and errorCount; keep existing
per-record error logging and calls to syncAccountToStripe unchanged. Ensure the
logging uses clear context (e.g., batch size and record IDs) and does not change
syncAccountToStripe semantics.
In `@ballerina-integrator/SFtoStripe/data_mappings.bal`:
- Line 45: The info-level logging call
log:printInfo("[mapContactToStripeCustomer] Building name", firstName =
firstName, lastName = lastName, fullName = fullName) inside the
mapContactToStripeCustomer routine is too verbose for production; change it to
use debug-level logging (log:printDebug) or remove the statement entirely so
mapping doesn't emit info logs for every contact—locate the log:printInfo call
in mapContactToStripeCustomer and replace or delete it accordingly.
In `@ballerina-integrator/SFtoStripe/README.md`:
- Around line 3-13: Convert plain-text section titles into proper Markdown
headers: replace "Description" with "# Description", "What it does" with "##
What it does", and similarly update other section titles mentioned in the file
(e.g., "Prerequisites", "Salesforce Setup", "Stripe Setup", "Configuration",
"Salesforce Configuration", "Stripe Configuration", "Deploying on Devant") to
appropriate heading levels (use H1 for the main title and H2/H3 for subsections)
so the README renders correctly; keep existing content under each title intact
and apply consistent heading levels across the document.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c97e2fae-7e42-49bf-a611-a56baec415d7
📒 Files selected for processing (11)
ballerina-integrator/SFtoStripe/Ballerina.tomlballerina-integrator/SFtoStripe/README.mdballerina-integrator/SFtoStripe/agents.balballerina-integrator/SFtoStripe/bulk_sync.balballerina-integrator/SFtoStripe/config.balballerina-integrator/SFtoStripe/connections.balballerina-integrator/SFtoStripe/data_mappings.balballerina-integrator/SFtoStripe/functions.balballerina-integrator/SFtoStripe/main.balballerina-integrator/SFtoStripe/types.balballerina-integrator/SFtoStripe/validation.bal
|
Prebuilt Integration Checklist
|
|
Please add the documentation files needed, and also give the integration a name that matches its action |
| } else { | ||
| log:printInfo("Delete handling disabled, skipping Stripe customer deletion", stripeCustomerId = stripeCustomerId); | ||
| } | ||
| } No newline at end of file |
| check syncContactToStripe(contact); | ||
| } | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ballerina-integrator/SFtoStripe/.choreo/component.yaml`:
- Around line 28-31: The schema currently defines salesforceRefreshUrl with a
default but does not include it in the schema's required list; update the
component.yaml schema to include "salesforceRefreshUrl" in the required array so
it matches the runtime config declaration (salesforceRefreshUrl) in config.bal
and prevents startup failures, ensuring the property name and type ("string")
exactly match the existing salesforceRefreshUrl entry.
In `@ballerina-integrator/SFtoStripe/.choreo/openapi.yaml`:
- Around line 100-130: The OpenAPI schema is missing fields required by runtime
filtering—update SalesforceAccountPayload and SalesforceContactPayload in
openapi.yaml to include the missing Salesforce fields used in types.bal and
filter logic: add RecordTypeId to both payloads, and add AccountStatus__c to
SalesforceAccountPayload (with type string and appropriate descriptions) so the
API contract matches the runtime code that references these fields.
- Around line 10-45: The OpenAPI spec lacks security definitions and a global
security requirement, leaving endpoints like operationId getMetrics public; add
a security scheme under components.securitySchemes (e.g., a bearerAuth or apiKey
entry) and then add a top-level security array to apply that scheme globally (so
all operations including /metrics and operationId getMetrics require it), only
adding explicit per-operation security entries on /health or others if you
intentionally want different rules; reference components.securitySchemes and the
global security object for the changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 53dad442-c588-41e6-8477-77a4cc490ca7
📒 Files selected for processing (4)
ballerina-integrator/SFtoStripe/.choreo/component.yamlballerina-integrator/SFtoStripe/.choreo/diagram.mdballerina-integrator/SFtoStripe/.choreo/instructions.mdballerina-integrator/SFtoStripe/.choreo/openapi.yaml
✅ Files skipped from review due to trivial changes (2)
- ballerina-integrator/SFtoStripe/.choreo/diagram.md
- ballerina-integrator/SFtoStripe/.choreo/instructions.md
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
ballerina-integrator/SFtoStripe/.choreo/diagram.md (1)
11-13: Consider labeling the post-validation branch conditions.After Line 11, both
D -> EandD -> Fexist without explicit conditions. Adding labels (e.g., “Stripe ID exists?”) would make the runtime decision path clearer for operators.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/.choreo/diagram.md` around lines 11 - 13, The diagram's post-validation branches from node D to E and D to F lack condition labels; update the diagram lines so the transitions explicitly state the decision (for example change "D --> E" to "D -- Stripe ID exists? --> E" and "D --> F" to "D -- Stripe ID missing? --> F" or use a boolean style like "-- [hasStripeId] -->" to make the runtime decision path clear to operators; ensure you only modify the two edges involving D->E and D->F in diagram.md.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ballerina-integrator/SFtoStripe/.choreo/diagram.md`:
- Around line 1-16: The Mermaid diagram is missing the required structure so it
won't parse; wrap the existing nodes/edges (A, B, C, D, E, F, G, H and the arrow
definitions) inside a mermaid code fence and add the "flowchart TD" declaration
at the top, then add classDef blocks for startNode, endNode, processNode, and
decisionNode to match the :::class annotations; keep the current node/edge text
and only prepend the flowchart TD line and the ```mermaid fence and append the
classDef definitions before closing the fence.
In `@ballerina-integrator/SFtoStripe/.choreo/instructions.md`:
- Around line 5-10: Update the deletion description to state that deleting
Stripe customers on Salesforce deletes is configurable rather than always
enabled: change the absolute "When records are deleted from Salesforce, the
corresponding Stripe customer is deleted" to something like "When records are
deleted from Salesforce, the corresponding Stripe customer can be deleted
depending on the deleteStripeCustomerOnSalesforceDelete configuration (default:
true)"; reference the deleteStripeCustomerOnSalesforceDelete flag and its
default value so operators know the behavior is configurable.
---
Nitpick comments:
In `@ballerina-integrator/SFtoStripe/.choreo/diagram.md`:
- Around line 11-13: The diagram's post-validation branches from node D to E and
D to F lack condition labels; update the diagram lines so the transitions
explicitly state the decision (for example change "D --> E" to "D -- Stripe ID
exists? --> E" and "D --> F" to "D -- Stripe ID missing? --> F" or use a boolean
style like "-- [hasStripeId] -->" to make the runtime decision path clear to
operators; ensure you only modify the two edges involving D->E and D->F in
diagram.md.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 077f3d5f-90cc-4cec-9d03-98e55a9bd669
📒 Files selected for processing (2)
ballerina-integrator/SFtoStripe/.choreo/diagram.mdballerina-integrator/SFtoStripe/.choreo/instructions.md
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
ballerina-integrator/SFtoStripe/data_mappings.bal (1)
17-19: Minor inconsistency in empty string checks.Line 17 checks
Email__c != ""before including, but Lines 18-19 don't apply the same empty-string guard forPhoneandDescription. This could result in empty strings being sent to Stripe.📝 Suggested fix
- if account?.Phone is string { payload["phone"] = account?.Phone; } - if account?.Description is string { payload["description"] = account?.Description; } + if account?.Phone is string && account?.Phone != "" { payload["phone"] = account?.Phone; } + if account?.Description is string && account?.Description != "" { payload["description"] = account?.Description; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/data_mappings.bal` around lines 17 - 19, Ensure consistency in string emptiness checks before populating the payload: the field Email__c already guards with `is string && != ""`, but `account?.Phone` and `account?.Description` only check `is string` and may send empty strings to Stripe. Update the checks for `account?.Phone` and `account?.Description` (the code that assigns to payload["phone"] and payload["description"]) to mirror the Email__c guard by verifying they are non-empty strings before assigning.ballerina-integrator/SFtoStripe/types.bal (1)
67-72: Remove the unusedStripeCustomerAddresstype.This type is not referenced anywhere in the codebase. The actual address payloads in
data_mappings.balusemap<json>with additional fields (state,postal_code) that aren't defined in this type, making it safe to delete.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/types.bal` around lines 67 - 72, Remove the unused StripeCustomerAddress type definition: delete the public type StripeCustomerAddress record {| string city?; string country?; string line1?; |}; since it is not referenced anywhere and actual address payloads use map<json> with extra fields (state, postal_code). Verify there are no remaining references to StripeCustomerAddress (e.g., in data_mappings.bal or other types) and remove or replace them if found; run a quick project-wide search for the symbol to ensure safe deletion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ballerina-integrator/SFtoStripe/functions.bal`:
- Around line 199-248: The isolated function searchStripeCustomerByMatchKey
illegally accesses the non-final module-level configurable matchKey; fix by
either marking the module-level variable matchKey as final/readonly or by
removing the isolated qualifier from the function signature of
searchStripeCustomerByMatchKey so it no longer requires isolation; update the
declaration for matchKey or the function signature consistently and run the
Ballerina build to ensure the isolation error is resolved.
In `@ballerina-integrator/SFtoStripe/main.bal`:
- Around line 282-309: The onRestore handler currently uses the partial CDC
payload (eventData.changedData) and calls data.cloneWithType() then
syncAccountToStripe/syncContactToStripe, which can miss Stripe_Customer_Id__c;
modify onRestore to fetch the full Salesforce record (as onCreate/onUpdate do)
using metadata.recordId before constructing the typed SalesforceAccount or
SalesforceContact, so the full record (including Stripe_Customer_Id__c) is
present for idempotent sync and then pass that full record into
syncAccountToStripe or syncContactToStripe.
---
Nitpick comments:
In `@ballerina-integrator/SFtoStripe/data_mappings.bal`:
- Around line 17-19: Ensure consistency in string emptiness checks before
populating the payload: the field Email__c already guards with `is string && !=
""`, but `account?.Phone` and `account?.Description` only check `is string` and
may send empty strings to Stripe. Update the checks for `account?.Phone` and
`account?.Description` (the code that assigns to payload["phone"] and
payload["description"]) to mirror the Email__c guard by verifying they are
non-empty strings before assigning.
In `@ballerina-integrator/SFtoStripe/types.bal`:
- Around line 67-72: Remove the unused StripeCustomerAddress type definition:
delete the public type StripeCustomerAddress record {| string city?; string
country?; string line1?; |}; since it is not referenced anywhere and actual
address payloads use map<json> with extra fields (state, postal_code). Verify
there are no remaining references to StripeCustomerAddress (e.g., in
data_mappings.bal or other types) and remove or replace them if found; run a
quick project-wide search for the symbol to ensure safe deletion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f20b15b2-2a7c-4819-b347-d315f11fe213
📒 Files selected for processing (8)
ballerina-integrator/SFtoStripe/.choreo/instructions.mdballerina-integrator/SFtoStripe/README.mdballerina-integrator/SFtoStripe/bulk_sync.balballerina-integrator/SFtoStripe/data_mappings.balballerina-integrator/SFtoStripe/functions.balballerina-integrator/SFtoStripe/main.balballerina-integrator/SFtoStripe/types.balballerina-integrator/SFtoStripe/validation.bal
🚧 Files skipped from review as they are similar to previous changes (1)
- ballerina-integrator/SFtoStripe/README.md
There was a problem hiding this comment.
Pull request overview
Adds a new Ballerina-based prebuilt integration module (SFtoStripe) that listens to Salesforce Change Data Capture (CDC) events for Account/Contact lifecycle changes and synchronizes corresponding Stripe Customers, with optional write-back of Stripe customer IDs and bulk sync utilities.
Changes:
- Introduces Salesforce CDC listener service handling create/update/delete/restore events and routing to Stripe sync logic.
- Adds Stripe customer create/update/delete logic with matching (email or external-id mode), idempotency protection, and Salesforce write-back.
- Adds configuration, type definitions, validation, bulk sync helpers, and deployment/setup documentation under
.choreo/.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ballerina-integrator/SFtoStripe/main.bal | CDC listener service and event handlers for Account/Contact sync flows. |
| ballerina-integrator/SFtoStripe/functions.bal | Core sync logic for Stripe customer create/update/delete, matching, and Salesforce write-back. |
| ballerina-integrator/SFtoStripe/data_mappings.bal | Mapping logic from Salesforce records to Stripe customer payloads + filter logic. |
| ballerina-integrator/SFtoStripe/validation.bal | Basic record validation (required IDs, optional email validation when matching by email). |
| ballerina-integrator/SFtoStripe/bulk_sync.bal | Bulk backfill sync for Accounts/Contacts based on sourceObject. |
| ballerina-integrator/SFtoStripe/config.bal | configurable settings for auth, sync direction, matching, filtering, and delete behavior. |
| ballerina-integrator/SFtoStripe/connections.bal | Global client creation for Salesforce and Stripe connectors. |
| ballerina-integrator/SFtoStripe/types.bal | Enums and record types for Salesforce entities (and one Stripe address type). |
| ballerina-integrator/SFtoStripe/README.md | User-facing setup/configuration guide for the integration. |
| ballerina-integrator/SFtoStripe/.choreo/instructions.md | Choreo/Devant deployment instructions and configuration reference. |
| ballerina-integrator/SFtoStripe/.choreo/diagram.md | Architecture/flow diagram content for the integration. |
| ballerina-integrator/SFtoStripe/Ballerina.toml | Package metadata for the new Ballerina module. |
| ballerina-integrator/SFtoStripe/agents.bal | Empty placeholder file added to the module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final salesforce:Client salesforceClient = check new ({ | ||
| baseUrl: salesforceBaseUrl, | ||
| auth: getSalesforceAuthConfig() | ||
| }); | ||
|
|
||
| // Stripe Client | ||
| final stripe:Client stripeClient = check new ({ |
There was a problem hiding this comment.
Module-level client initialization uses check new (...). check requires an error-returning context; at module scope this will fail to compile (or panic semantics won’t be explicit). Initialize the Salesforce client in an init() function that returns error?, or use checkpanic/an |error-typed variable with explicit handling so startup failures are handled deterministically.
| final salesforce:Client salesforceClient = check new ({ | |
| baseUrl: salesforceBaseUrl, | |
| auth: getSalesforceAuthConfig() | |
| }); | |
| // Stripe Client | |
| final stripe:Client stripeClient = check new ({ | |
| final salesforce:Client salesforceClient = checkpanic new ({ | |
| baseUrl: salesforceBaseUrl, | |
| auth: getSalesforceAuthConfig() | |
| }); | |
| // Stripe Client | |
| final stripe:Client stripeClient = checkpanic new ({ |
| final salesforce:Client salesforceClient = check new ({ | ||
| baseUrl: salesforceBaseUrl, | ||
| auth: getSalesforceAuthConfig() | ||
| }); | ||
|
|
||
| // Stripe Client | ||
| final stripe:Client stripeClient = check new ({ |
There was a problem hiding this comment.
Module-level Stripe client initialization uses check new (...). As with the Salesforce client, check is not valid at module scope; move this to an init() that can return error?, or make the failure mode explicit (e.g., checkpanic or Client|error).
| final salesforce:Client salesforceClient = check new ({ | |
| baseUrl: salesforceBaseUrl, | |
| auth: getSalesforceAuthConfig() | |
| }); | |
| // Stripe Client | |
| final stripe:Client stripeClient = check new ({ | |
| final salesforce:Client salesforceClient = checkpanic new ({ | |
| baseUrl: salesforceBaseUrl, | |
| auth: getSalesforceAuthConfig() | |
| }); | |
| // Stripe Client | |
| final stripe:Client stripeClient = checkpanic new ({ |
| // Detect if this is actually a delete event mislabelled as update | ||
| json changeTypeVal = changedFields["ChangeEventHeader"] is map<json> | ||
| ? ((<map<json>>changedFields["ChangeEventHeader"])["changeType"] ?: "") | ||
| : ""; | ||
| log:printInfo("[onUpdate] changeType from ChangeEventHeader: " + changeTypeVal.toString()); |
There was a problem hiding this comment.
changeTypeVal is computed/logged with a comment about detecting mislabelled delete events, but the value is never used and no branching is performed. Either implement the intended handling (e.g., route to delete when changeType indicates DELETE) or remove the unused variable/logging to reduce noise.
| // Detect if this is actually a delete event mislabelled as update | |
| json changeTypeVal = changedFields["ChangeEventHeader"] is map<json> | |
| ? ((<map<json>>changedFields["ChangeEventHeader"])["changeType"] ?: "") | |
| : ""; | |
| log:printInfo("[onUpdate] changeType from ChangeEventHeader: " + changeTypeVal.toString()); |
Shall we fix these |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
ballerina-integrator/SFtoStripe/functions.bal (1)
199-248:⚠️ Potential issue | 🔴 CriticalIsolation violation:
isolatedfunction accesses non-finalmatchKeyconfigurable.This function is marked
isolatedbut accesses the module-levelmatchKeyconfigurable (lines 201, 219), which violates Ballerina's isolation constraints. This was flagged in a previous review but the fix appears incomplete.🔧 Proposed fix: remove isolated qualifier
-isolated function searchStripeCustomerByMatchKey(string? salesforceId, string? email, string? externalId) returns string?|error { +function searchStripeCustomerByMatchKey(string? salesforceId, string? email, string? externalId) returns string?|error {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/functions.bal` around lines 199 - 248, The function searchStripeCustomerByMatchKey is marked isolated but reads the non-final module-level configurable matchKey (violates isolation); fix by either removing the isolated qualifier from searchStripeCustomerByMatchKey or by making matchKey final/const and not configurable, or better yet pass matchKey in as an explicit parameter to searchStripeCustomerByMatchKey and use that local value instead—choose one approach and update all call sites accordingly.
🧹 Nitpick comments (3)
ballerina-integrator/SFtoStripe/data_mappings.bal (1)
100-141: Consider usingarray:indexOfto simplify filter matching.The manual
foreachloops for checking filter membership could be simplified usingarray:indexOf:♻️ Optional refactor using array:indexOf
+import ballerina/lang.array; + // Check if record passes filters public function passFilters(string? recordTypeId, string? accountStatus) returns boolean { // Check RecordType filter if recordTypeFilter.length() > 0 { if recordTypeId is () { log:printDebug("Record filtered out: No RecordTypeId"); return false; } - boolean recordTypeMatch = false; - foreach string allowedType in recordTypeFilter { - if recordTypeId == allowedType { - recordTypeMatch = true; - break; - } - } - if !recordTypeMatch { + if array:indexOf(recordTypeFilter, recordTypeId) is () { log:printDebug("Record filtered out: RecordTypeId does not match filter"); return false; } } // Check AccountStatus filter if accountStatusFilter.length() > 0 { if accountStatus is () { log:printDebug("Record filtered out: No AccountStatus"); return false; } - boolean statusMatch = false; - foreach string allowedStatus in accountStatusFilter { - if accountStatus == allowedStatus { - statusMatch = true; - break; - } - } - if !statusMatch { + if array:indexOf(accountStatusFilter, accountStatus) is () { log:printDebug("Record filtered out: AccountStatus does not match filter"); return false; } } return true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/data_mappings.bal` around lines 100 - 141, The passFilters function currently uses manual foreach loops to check membership against recordTypeFilter and accountStatusFilter; replace those loops with array:indexOf to simplify and clarify the checks: for RecordType use array:indexOf(recordTypeFilter, recordTypeId) to test presence (and similarly for AccountStatus with array:indexOf(accountStatusFilter, accountStatus)), keeping the existing early-return behavior and log messages (symbols to update: passFilters, recordTypeFilter, accountStatusFilter, recordTypeId, accountStatus, and the debug log calls).ballerina-integrator/SFtoStripe/README.md (2)
1-12: Markdown headers are missing formatting.The section titles (Description, What it does, Prerequisites, etc.) should use proper Markdown header syntax (
##or###) for better rendering and navigation.📝 Proposed fix for header formatting
# Salesforce to Stripe Customer Sync Integration -Description +## Description This integration listens for Salesforce Account and Contact creation, update, and deletion events and creates or updates corresponding customers in Stripe. -What it does +## What it does - When an Account or Contact is created in Salesforce, the integration creates a customer in Stripe with the customer details🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/README.md` around lines 1 - 12, The README uses plain text section titles (e.g., "Description", "What it does") instead of Markdown headers; update those lines to use proper header syntax (for example, convert "Description" and "What it does" to "## Description" and "## What it does" or appropriate "###" levels) so the sections render and appear in the document outline; ensure all other section titles in the file follow the same pattern (e.g., "Prerequisites", "Usage") for consistent formatting.
14-60: Continue formatting headers and fix minor inconsistency.Apply the same header formatting throughout, and add missing colon on line 51.
📝 Proposed formatting fixes
-Prerequisites +## Prerequisites Before running this integration, you need: -Salesforce Setup +### Salesforce Setup - Enable Change Data Capture for Account and Contact objects: - Navigate to Setup > Change Data Capture - Move Account and Contact to "Selected Entities" - Click Save - Create a custom field `Stripe_Customer_Id__c` (Text, 255 chars) on Account and Contact objects - (Optional) Create a custom field `Email__c` (Email, 255 chars) on Account object if you want to sync account emails to Stripe - (Optional) Create a custom field `AccountStatus__c` on Account object if you want to use the accountStatusFilter configuration - Create a Connected App for OAuth2 credentials: - Navigate to Setup > Apps > App Manager - Create new Connected App and note the Client ID, Client Secret, and Refresh Token -Stripe Setup +### Stripe Setup - Log in to your Stripe account and navigate to the Developers section - Click on API keys in the left sidebar - Copy the value of the Secret key -Configuration +## Configuration The following configurations are required for the integration: -Salesforce Configuration +### Salesforce Configuration - salesforceRefreshToken: Your Salesforce OAuth2 refresh token - salesforceClientId: Your Salesforce OAuth2 client ID - salesforceClientSecret: Your Salesforce OAuth2 client secret - salesforceRefreshUrl: Salesforce OAuth2 token endpoint (https://login.salesforce.com/services/oauth2/token) - salesforceBaseUrl: Your Salesforce instance URL (https://your-org.my.salesforce.com) -Stripe Configuration +### Stripe Configuration + - stripeApiKey: The Stripe Secret API key obtained from the Stripe setup -Deploying on Devant +## Deploying on Devant🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ballerina-integrator/SFtoStripe/README.md` around lines 14 - 60, Make the README headers consistent by converting each top-level section title ("Prerequisites", "Salesforce Setup", "Stripe Setup", "Configuration", "Salesforce Configuration", "Stripe Configuration", "Deploying on Devant") to the same Markdown heading style (e.g., use "##" for each top-level section), ensure there is a blank line after each heading, normalize sub-sections/lists to the same bullet format, and add the missing colon to the "Stripe Configuration" header so it reads with a trailing colon like the other configuration headings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ballerina-integrator/SFtoStripe/bulk_sync.bal`:
- Around line 17-48: The Email__c fallback path can still fail if
AccountStatus__c is also missing; update the branch that handles
accountStreamResult.message().includes("Email__c") to attempt a nested fallback:
after running salesforceClient->query(soqlQueryNoEmail) check if that
fallbackResult is error, inspect fallbackResult.message() for "AccountStatus__c"
and if present call salesforceClient->query(soqlQueryMinimal) (using the same
pattern you used for the other branch, e.g., accountStream = check
salesforceClient->query(soqlQueryMinimal)); otherwise return the original
fallbackResult; keep existing successful-assignment logic when fallbackResult is
a stream.
In `@ballerina-integrator/SFtoStripe/functions.bal`:
- Around line 99-113: The SOQL string in the syncContactToStripe re-fetch block
interpolates contactId directly (inside the function handling Contact sync,
e.g., syncContactToStripe), which is unsafe if contact.Id is empty or malformed;
validate contactId before building soqlQuery (ensure it's non-empty and matches
expected Salesforce ID format/length), log and return early if invalid, or use a
safe parameter/escaping approach if available in salesforceClient, then only
construct and call salesforceClient->query(soqlQuery) when contactId is
validated to avoid injection or malformed queries.
- Around line 27-39: The SOQL construction uses string interpolation with
external input (accountId) which can allow injection; update the logic in
syncAccountToStripe (the block using accountId, soqlQuery,
salesforceClient->query and queryResult.next) to validate accountId against
Salesforce ID format (e.g., 15/18‑char alphanumeric) before building soqlQuery,
and if validation fails, log and return; if your Salesforce client supports
parameterized queries use that instead of string interpolation when calling
salesforceClient->query to eliminate injection risk.
- Around line 269-299: The functions handleAccountDeletion and
handleContactDeletion are declared isolated but read the mutable module-level
configurable deleteStripeCustomerOnSalesforceDelete, causing an isolation
violation; fix by removing the isolated qualifier from both function
declarations (i.e., change "public isolated function handleAccountDeletion" and
"public isolated function handleContactDeletion" to non-isolated public
functions) so they can legally access the configurable, keeping all other logic
intact.
---
Duplicate comments:
In `@ballerina-integrator/SFtoStripe/functions.bal`:
- Around line 199-248: The function searchStripeCustomerByMatchKey is marked
isolated but reads the non-final module-level configurable matchKey (violates
isolation); fix by either removing the isolated qualifier from
searchStripeCustomerByMatchKey or by making matchKey final/const and not
configurable, or better yet pass matchKey in as an explicit parameter to
searchStripeCustomerByMatchKey and use that local value instead—choose one
approach and update all call sites accordingly.
---
Nitpick comments:
In `@ballerina-integrator/SFtoStripe/data_mappings.bal`:
- Around line 100-141: The passFilters function currently uses manual foreach
loops to check membership against recordTypeFilter and accountStatusFilter;
replace those loops with array:indexOf to simplify and clarify the checks: for
RecordType use array:indexOf(recordTypeFilter, recordTypeId) to test presence
(and similarly for AccountStatus with array:indexOf(accountStatusFilter,
accountStatus)), keeping the existing early-return behavior and log messages
(symbols to update: passFilters, recordTypeFilter, accountStatusFilter,
recordTypeId, accountStatus, and the debug log calls).
In `@ballerina-integrator/SFtoStripe/README.md`:
- Around line 1-12: The README uses plain text section titles (e.g.,
"Description", "What it does") instead of Markdown headers; update those lines
to use proper header syntax (for example, convert "Description" and "What it
does" to "## Description" and "## What it does" or appropriate "###" levels) so
the sections render and appear in the document outline; ensure all other section
titles in the file follow the same pattern (e.g., "Prerequisites", "Usage") for
consistent formatting.
- Around line 14-60: Make the README headers consistent by converting each
top-level section title ("Prerequisites", "Salesforce Setup", "Stripe Setup",
"Configuration", "Salesforce Configuration", "Stripe Configuration", "Deploying
on Devant") to the same Markdown heading style (e.g., use "##" for each
top-level section), ensure there is a blank line after each heading, normalize
sub-sections/lists to the same bullet format, and add the missing colon to the
"Stripe Configuration" header so it reads with a trailing colon like the other
configuration headings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2c92a090-addf-487f-8449-975414ac13f3
📒 Files selected for processing (9)
ballerina-integrator/SFtoStripe/.choreo/instructions.mdballerina-integrator/SFtoStripe/README.mdballerina-integrator/SFtoStripe/bulk_sync.balballerina-integrator/SFtoStripe/config.balballerina-integrator/SFtoStripe/data_mappings.balballerina-integrator/SFtoStripe/functions.balballerina-integrator/SFtoStripe/main.balballerina-integrator/SFtoStripe/types.balballerina-integrator/SFtoStripe/validation.bal
✅ Files skipped from review due to trivial changes (1)
- ballerina-integrator/SFtoStripe/.choreo/instructions.md
🚧 Files skipped from review as they are similar to previous changes (4)
- ballerina-integrator/SFtoStripe/config.bal
- ballerina-integrator/SFtoStripe/types.bal
- ballerina-integrator/SFtoStripe/validation.bal
- ballerina-integrator/SFtoStripe/main.bal
| // If this is a create event (not update) and no Stripe ID exists yet, | ||
| // re-fetch the contact to check if another concurrent event already created the customer | ||
| if !isUpdate && (existingStripeId is () || existingStripeId == "") { | ||
| string contactId = contact?.Id ?: ""; | ||
| string soqlQuery = string `SELECT Stripe_Customer_Id__c FROM Contact WHERE Id = '${contactId}'`; | ||
| stream<SalesforceContact, error?> queryResult = check salesforceClient->query(soqlQuery); | ||
| record {|SalesforceContact value;|}? queryRecord = check queryResult.next(); | ||
| if queryRecord is record {|SalesforceContact value;|} { | ||
| string? refetchedStripeId = queryRecord.value?.Stripe_Customer_Id__c; | ||
| if refetchedStripeId is string && refetchedStripeId != "" { | ||
| log:printInfo("[syncContactToStripe] Stripe ID already exists (concurrent event), skipping", contactId = contactId, stripeCustomerId = refetchedStripeId); | ||
| return; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Same SOQL interpolation concern as Account sync.
The Contact re-fetch query at line 103 has the same string interpolation pattern. Apply similar ID validation if addressing the Account case.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ballerina-integrator/SFtoStripe/functions.bal` around lines 99 - 113, The
SOQL string in the syncContactToStripe re-fetch block interpolates contactId
directly (inside the function handling Contact sync, e.g., syncContactToStripe),
which is unsafe if contact.Id is empty or malformed; validate contactId before
building soqlQuery (ensure it's non-empty and matches expected Salesforce ID
format/length), log and return early if invalid, or use a safe
parameter/escaping approach if available in salesforceClient, then only
construct and call salesforceClient->query(soqlQuery) when contactId is
validated to avoid injection or malformed queries.
| // Handle Salesforce Account deletion | ||
| public isolated function handleAccountDeletion(SalesforceAccount account) returns error? { | ||
| string? stripeCustomerId = account?.Stripe_Customer_Id__c; | ||
|
|
||
| if stripeCustomerId is () || stripeCustomerId == "" { | ||
| log:printInfo("Account has no Stripe Customer ID, nothing to delete", accountId = account?.Id); | ||
| return; | ||
| } | ||
|
|
||
| if deleteStripeCustomerOnSalesforceDelete { | ||
| check deleteStripeCustomer(stripeCustomerId); | ||
| } else { | ||
| log:printInfo("Delete handling disabled, skipping Stripe customer deletion", stripeCustomerId = stripeCustomerId); | ||
| } | ||
| } | ||
|
|
||
| // Handle Salesforce Contact deletion | ||
| public isolated function handleContactDeletion(SalesforceContact contact) returns error? { | ||
| string? stripeCustomerId = contact?.Stripe_Customer_Id__c; | ||
|
|
||
| if stripeCustomerId is () || stripeCustomerId == "" { | ||
| log:printInfo("Contact has no Stripe Customer ID, nothing to delete", contactId = contact?.Id); | ||
| return; | ||
| } | ||
|
|
||
| if deleteStripeCustomerOnSalesforceDelete { | ||
| check deleteStripeCustomer(stripeCustomerId); | ||
| } else { | ||
| log:printInfo("Delete handling disabled, skipping Stripe customer deletion", stripeCustomerId = stripeCustomerId); | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ballerina configurable variables isolation constraints
💡 Result:
In Ballerina, configurable variables (declared with the configurable keyword, e.g., configurable int port = ?;) have no specific isolation constraints documented in official sources. They are module-level variables that can hold configuration values provided via TOML files, environment variables, or CLI arguments, with override precedence: environment variables > CLI args > TOML. Ballerina's concurrency model uses isolated qualifiers on functions, methods, objects, and variables to ensure thread-safety by preventing data races on shared mutable state at compile time. Official docs cover isolated variables separately from configurables, with no mention of configurables being inherently isolated or restricted (e.g., cannot be isolated). Configurable variables are initialized before module init, which may affect complex types like records referencing other configurables, but this is an initialization issue, not isolation-related. For concurrency safety in services using configurables, mark services/resources as isolated and use lock statements or readonly types as needed. The Ballerina constraint module provides validation annotations (e.g., @constraint:String), unrelated to concurrency isolation. No evidence of special isolation constraints for configurables; treat them as regular module variables requiring explicit isolation for concurrent access.
Citations:
- 1: https://ballerina.io/learn/provide-values-to-configurable-variables
- 2: https://ballerina.io/learn/by-example/configurable-variables/
- 3: https://ballerina.io/learn/vs-code-extension/references/statement-editor/define-a-configurable-variable
- 4: https://ballerina.io/learn/configure-values/
- 5: https://ballerina.io/learn/by-example/configurable-variables
- 6: https://ballerina.io/spec/lang/2022R4
- 7: https://ballerina.io/learn/by-example/isolated-variables/
- 8: https://ballerina.io/learn/by-example/isolated-variables
- 9: https://medium.com/ballerina-techblog/ballerina-concurrency-no-more-ticking-time-bombs-4a360ac4b1ab
- 10: Configurable record field that refers to another constant/configurable variable returns null value ballerina-platform/ballerina-lang#28966
🏁 Script executed:
rg "deleteStripeCustomerOnSalesforceDelete" -B 2 -A 2Repository: wso2/integration-samples
Length of output: 1579
Isolation violation: isolated functions access mutable deleteStripeCustomerOnSalesforceDelete configurable.
Both handleAccountDeletion and handleContactDeletion are marked isolated but access the module-level deleteStripeCustomerOnSalesforceDelete configurable on lines 278 and 294. Configurable variables are not inherently isolated or readonly and are treated as regular mutable module-level variables in Ballerina's concurrency model.
🔧 Proposed fix: remove isolated qualifiers
-public isolated function handleAccountDeletion(SalesforceAccount account) returns error? {
+public function handleAccountDeletion(SalesforceAccount account) returns error? {-public isolated function handleContactDeletion(SalesforceContact contact) returns error? {
+public function handleContactDeletion(SalesforceContact contact) returns error? {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ballerina-integrator/SFtoStripe/functions.bal` around lines 269 - 299, The
functions handleAccountDeletion and handleContactDeletion are declared isolated
but read the mutable module-level configurable
deleteStripeCustomerOnSalesforceDelete, causing an isolation violation; fix by
removing the isolated qualifier from both function declarations (i.e., change
"public isolated function handleAccountDeletion" and "public isolated function
handleContactDeletion" to non-isolated public functions) so they can legally
access the configurable, keeping all other logic intact.
Purpose
Resolves https://github.com/issues/assigned?issue=wso2-enterprise%7Cintegration-engineering%7C61 - Create a prebuilt integration to automatically sync Salesforce customers with Stripe for streamlined payment processing and customer data management.
Goals
Approach
Implemented a prebuilt integration using WSO2 Integration Studio that:
Summary by CodeRabbit
New Features
Documentation
Validation & Config