Skip to content

Add Salesforce to Stripe Customer Sync Integration - #69

Open
Dulavinya wants to merge 6 commits into
wso2:mainfrom
Dulavinya:SFtoStripe
Open

Add Salesforce to Stripe Customer Sync Integration#69
Dulavinya wants to merge 6 commits into
wso2:mainfrom
Dulavinya:SFtoStripe

Conversation

@Dulavinya

@Dulavinya Dulavinya commented Mar 13, 2026

Copy link
Copy Markdown

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

  • Develop a prebuilt integration connector between Salesforce and Stripe
  • Enable automatic synchronization of customer data from Salesforce to Stripe
  • Provide a reusable integration template for WSO2 customers
  • Reduce manual customer data entry and synchronization efforts

Approach

Implemented a prebuilt integration using WSO2 Integration Studio that:

  • Connects to Salesforce APIs to retrieve customer records
  • Transforms customer data to Stripe format
  • Syncs customer information to Stripe platform
  • Includes error handling and retry mechanisms

Summary by CodeRabbit

  • New Features

    • Salesforce → Stripe customer sync: real-time CDC listeners, bulk sync, idempotent create/update, delete handling, and optional Stripe ID write-back; configurable source/matching filters.
  • Documentation

    • Full setup guides, configuration reference, and architecture/flow diagram added.
  • Validation & Config

    • Runtime-configurable auth and sync settings, record/email validation, and filter controls exposed via configuration.

@CLAassistant

CLAassistant commented Mar 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds 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

Cohort / File(s) Summary
Project Manifest
ballerina-integrator/SFtoStripe/Ballerina.toml
Adds package metadata (org, name, version, distribution).
Configuration
ballerina-integrator/SFtoStripe/config.bal
Introduces configurable Salesforce/Stripe credentials and sync controls; helper functions returning OAuth2 and bearer token configs.
Client Connections
ballerina-integrator/SFtoStripe/connections.bal
Module-level Salesforce and Stripe client initialization using config helpers (constructed at module load).
Types
ballerina-integrator/SFtoStripe/types.bal
Adds enums (SyncDirection, SourceObject, MatchKey) and record types for SalesforceAccount, SalesforceContact, StripeCustomerAddress.
Validation
ballerina-integrator/SFtoStripe/validation.bal
Adds account/contact validators and email-format checker with warnings/errors based on matchKey.
Data Mapping & Filters
ballerina-integrator/SFtoStripe/data_mappings.bal
Adds mappers from SF Account/Contact to Stripe customer payloads, address builder, conditional field helpers, and record-type/account-status filtering logic.
Sync Logic & Helpers
ballerina-integrator/SFtoStripe/functions.bal
Core sync functions for Account/Contact (create/update with idempotency, match-key lookup, optional write-back), deletion handlers, search helper, and write-back helper.
Bulk Sync
ballerina-integrator/SFtoStripe/bulk_sync.bal
Adds streaming bulk sync entrypoints for Accounts/Contacts with progressive SOQL fallbacks and per-record success/error counting.
CDC Listener & Service
ballerina-integrator/SFtoStripe/main.bal
Adds Salesforce Change Data Capture listener and "/data/ChangeEvents" service with onCreate/onUpdate/onDelete/onRestore handlers that fetch records, apply fallbacks, and invoke sync flows.
Docs & Diagrams
ballerina-integrator/SFtoStripe/README.md, ballerina-integrator/SFtoStripe/.choreo/diagram.md, ballerina-integrator/SFtoStripe/.choreo/instructions.md
Adds README, choreography diagram, and setup instructions documenting CDC-driven sync, prerequisites, configuration, and deployment steps.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

In a burrow of code I hop and hum,
CDC whispers — new customers come,
Map and validate, create with care,
Write back IDs and tidy the pair,
A rabbit cheers the sync we drum! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description addresses Purpose, Goals, and Approach sections from the template, but is missing critical sections like Release notes, Documentation, Security checks, Automation tests, and others required by the template. Complete the PR description by adding all required template sections including Release notes, Documentation links, Security checks confirmation, and Automation tests details to meet the repository's contribution standards.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Salesforce to Stripe customer synchronization integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:printInfo on every contact mapping is verbose for production. Consider removing it or using log:printDebug instead.

♻️ 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 details

Apply 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

📥 Commits

Reviewing files that changed from the base of the PR and between c7cb295 and a28cb30.

📒 Files selected for processing (11)
  • ballerina-integrator/SFtoStripe/Ballerina.toml
  • ballerina-integrator/SFtoStripe/README.md
  • ballerina-integrator/SFtoStripe/agents.bal
  • ballerina-integrator/SFtoStripe/bulk_sync.bal
  • ballerina-integrator/SFtoStripe/config.bal
  • ballerina-integrator/SFtoStripe/connections.bal
  • ballerina-integrator/SFtoStripe/data_mappings.bal
  • ballerina-integrator/SFtoStripe/functions.bal
  • ballerina-integrator/SFtoStripe/main.bal
  • ballerina-integrator/SFtoStripe/types.bal
  • ballerina-integrator/SFtoStripe/validation.bal

Comment thread ballerina-integrator/SFtoStripe/bulk_sync.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/config.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/data_mappings.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/functions.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/functions.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/main.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/main.bal
Comment thread ballerina-integrator/SFtoStripe/main.bal
Comment thread ballerina-integrator/SFtoStripe/validation.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/validation.bal Outdated
@hasathcharu

Copy link
Copy Markdown
Contributor

Prebuilt Integration Checklist

  • github/workflows/projects.json is updated with the new prebuilt integration path
  • Path in the .github/workflows/projects.json matches the project path exactly
  • Directory path is identical to the integration name in Ballerina.toml
  • README.MD is correctly structured
  • Configurations are organized by records based on vendor
  • choreo/config-schema.json exists and is up to date
  • choreo/instructions.md is correctly structured
  • choreo/diagram.md is correctly written and parsable by Mermaid
  • choreo/component.yaml exists for service type integrations
  • choreo/openapi.yaml exists for service type integrations

@hasathcharu

Copy link
Copy Markdown
Contributor

Please add the documentation files needed, and also give the integration a name that matches its action

Comment thread ballerina-integrator/SFtoStripe/types.bal
Comment thread ballerina-integrator/SFtoStripe/validation.bal
Comment thread ballerina-integrator/SFtoStripe/bulk_sync.bal
Comment thread ballerina-integrator/SFtoStripe/connections.bal
} else {
log:printInfo("Delete handling disabled, skipping Stripe customer deletion", stripeCustomerId = stripeCustomerId);
}
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

check syncContactToStripe(contact);
}
}
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Comment thread ballerina-integrator/SFtoStripe/.choreo/component.yaml Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/diagram.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/diagram.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/openapi.yaml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a28cb30 and 438fe55.

📒 Files selected for processing (4)
  • ballerina-integrator/SFtoStripe/.choreo/component.yaml
  • ballerina-integrator/SFtoStripe/.choreo/diagram.md
  • ballerina-integrator/SFtoStripe/.choreo/instructions.md
  • ballerina-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

Comment thread ballerina-integrator/SFtoStripe/.choreo/component.yaml Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/openapi.yaml Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/openapi.yaml Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -> E and D -> F exist 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

📥 Commits

Reviewing files that changed from the base of the PR and between 438fe55 and 0e2a199.

📒 Files selected for processing (2)
  • ballerina-integrator/SFtoStripe/.choreo/diagram.md
  • ballerina-integrator/SFtoStripe/.choreo/instructions.md

Comment thread ballerina-integrator/SFtoStripe/.choreo/diagram.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for Phone and Description. 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 unused StripeCustomerAddress type.

This type is not referenced anywhere in the codebase. The actual address payloads in data_mappings.bal use map<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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2a199 and 28a16d7.

📒 Files selected for processing (8)
  • ballerina-integrator/SFtoStripe/.choreo/instructions.md
  • ballerina-integrator/SFtoStripe/README.md
  • ballerina-integrator/SFtoStripe/bulk_sync.bal
  • ballerina-integrator/SFtoStripe/data_mappings.bal
  • ballerina-integrator/SFtoStripe/functions.bal
  • ballerina-integrator/SFtoStripe/main.bal
  • ballerina-integrator/SFtoStripe/types.bal
  • ballerina-integrator/SFtoStripe/validation.bal
🚧 Files skipped from review as they are similar to previous changes (1)
  • ballerina-integrator/SFtoStripe/README.md

Comment thread ballerina-integrator/SFtoStripe/functions.bal
Comment thread ballerina-integrator/SFtoStripe/main.bal

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +11
final salesforce:Client salesforceClient = check new ({
baseUrl: salesforceBaseUrl,
auth: getSalesforceAuthConfig()
});

// Stripe Client
final stripe:Client stripeClient = check new ({

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 ({

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +11
final salesforce:Client salesforceClient = check new ({
baseUrl: salesforceBaseUrl,
auth: getSalesforceAuthConfig()
});

// Stripe Client
final stripe:Client stripeClient = check new ({

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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 ({

Copilot uses AI. Check for mistakes.
Comment thread ballerina-integrator/SFtoStripe/config.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/functions.bal
Comment on lines +133 to +137
// 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());

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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());

Copilot uses AI. Check for mistakes.
Comment thread ballerina-integrator/SFtoStripe/README.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md
@hasathcharu

Copy link
Copy Markdown
Contributor

Prebuilt Integration Checklist

  • github/workflows/projects.json is updated with the new prebuilt integration path
  • Path in the .github/workflows/projects.json matches the project path exactly
  • Directory path is identical to the integration name in Ballerina.toml
  • README.MD is correctly structured
  • Configurations are organized by records based on vendor
  • choreo/config-schema.json exists and is up to date
  • choreo/instructions.md is correctly structured
  • choreo/diagram.md is correctly written and parsable by Mermaid
  • choreo/component.yaml exists for service type integrations
  • choreo/openapi.yaml exists for service type integrations

Shall we fix these

Comment thread ballerina-integrator/SFtoStripe/data_mappings.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/data_mappings.bal
Comment thread ballerina-integrator/SFtoStripe/data_mappings.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/data_mappings.bal
Comment thread ballerina-integrator/SFtoStripe/functions.bal
Comment thread ballerina-integrator/SFtoStripe/validation.bal
Comment thread ballerina-integrator/SFtoStripe/types.bal Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
ballerina-integrator/SFtoStripe/functions.bal (1)

199-248: ⚠️ Potential issue | 🔴 Critical

Isolation violation: isolated function accesses non-final matchKey configurable.

This function is marked isolated but accesses the module-level matchKey configurable (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 using array:indexOf to simplify filter matching.

The manual foreach loops for checking filter membership could be simplified using array: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

📥 Commits

Reviewing files that changed from the base of the PR and between 28a16d7 and cd8126a.

📒 Files selected for processing (9)
  • ballerina-integrator/SFtoStripe/.choreo/instructions.md
  • ballerina-integrator/SFtoStripe/README.md
  • ballerina-integrator/SFtoStripe/bulk_sync.bal
  • ballerina-integrator/SFtoStripe/config.bal
  • ballerina-integrator/SFtoStripe/data_mappings.bal
  • ballerina-integrator/SFtoStripe/functions.bal
  • ballerina-integrator/SFtoStripe/main.bal
  • ballerina-integrator/SFtoStripe/types.bal
  • ballerina-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

Comment thread ballerina-integrator/SFtoStripe/bulk_sync.bal Outdated
Comment thread ballerina-integrator/SFtoStripe/functions.bal
Comment on lines +99 to +113
// 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;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +269 to +299
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


🏁 Script executed:

rg "deleteStripeCustomerOnSalesforceDelete" -B 2 -A 2

Repository: 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.

Comment thread ballerina-integrator/SFtoStripe/.choreo/diagram.md Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md Outdated
Comment thread ballerina-integrator/SFtoStripe/Ballerina.toml Outdated
Comment thread ballerina-integrator/SFtoStripe/.choreo/instructions.md Outdated
Comment thread ballerina-integrator/SFtoStripe/bulk_sync.bal
Comment thread ballerina-integrator/SFtoStripe/config.bal Outdated
@Dulavinya Dulavinya changed the title Added files for the SFtoStripe. Add Salesforce to Stripe Customer Sync Integration Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants