# Technical Specification: Salesforce Connector for iPaaS Platform

**Version**: 1.0
**Date**: March 26, 2026
**Status**: Ready for Development

---

## Business Value

Current SuiteX customers cannot integrate Salesforce with NetSuite/other systems through iPaaS, forcing them to use separate tools ($500-2K/month), build custom integrations (40-80 hours each), or manually sync data (20-40 hours/month). This creates data inconsistencies and increases operational costs.

Adding native Salesforce support enables $15-25K ARR upsell per customer, positions SuiteX against Workato/Celigo for CRM-ERP integrations, and reduces customer integration costs by 60-80% vs custom development.

---

## Context

The iPaaS platform uses a **strategy pattern** for connectors: each authentication type (OAuth1, OAuth2, Basic, JWT, Password) has a strategy class, and within each strategy, the `application` field differentiates API-specific behaviors. Existing examples:
- **SnowflakeService** (JWT): Custom JWT generation + async query polling (~400 lines)
- **NetsuiteService** (OAuth1): OAuth1 signature generation (~56 lines)
- **WalmartSignatureAuth** (Basic): Custom HMAC signing (~136 lines)

Salesforce follows this pattern: OAuth2 authentication strategy with `salesforce_oauth2` application type routes to `SalesforceService` for Salesforce-specific logic (instance URL handling, API versioning, pagination).

**Current iPaaS scope** supports: connector creation/auth, flow execution, API nodes, pagination, throttling, error retry, token refresh. Salesforce extends this with SOQL query support and REST CRUD operations.

---

## Deliverables

Extend the iPaaS platform with native Salesforce REST API and SOQL query support by adding six capabilities:

**API Surface** (2):
1. OAuth2 authentication with instance URL extraction
2. SOQL query execution with automatic pagination

**CRUD Operations** (3):
3. Create records (POST)
4. Read records (GET)
5. Update/Upsert records (PATCH)

**Infrastructure** (1):
6. Automatic token refresh, rate limiting, and error handling

---

## Deliverable Details

### 1. OAuth2 Authentication with Instance URL Extraction

**Endpoint Pattern**: Standard OAuth2 Web Server Flow
**Purpose**: Authenticate Salesforce connectors and extract the dynamic instance URL for API calls.

**Behavior**:
- User creates connector via iPaaS UI, selects "Salesforce Production" or "Salesforce Sandbox" application type
- System initiates OAuth redirect to `login.salesforce.com` (production) or `test.salesforce.com` (sandbox)
- After authorization, OAuth callback receives: `access_token`, `refresh_token`, `instance_url`
- Instance URL (e.g., `https://na50.salesforce.com`) is extracted and stored in `connector_config.instance_url`
- All subsequent API calls use instance URL as base URL instead of connector.base_url

**Response Schema** (OAuth callback):
```json
{
  "access_token": "00D...",
  "refresh_token": "5Aep...",
  "instance_url": "https://na50.salesforce.com",
  "id": "https://login.salesforce.com/id/00D.../005...",
  "token_type": "Bearer",
  "issued_at": "1647360000000"
}
```

---

### 2. SOQL Query Execution with Pagination

**Endpoint**: `GET /services/data/v59.0/query/?q={soql_query}`
**Purpose**: Execute Salesforce Object Query Language (SOQL) queries to retrieve data.

**Behavior**:
- API Node configured with query parameter: `q=SELECT Id, Name FROM Contact WHERE CreatedDate = TODAY`
- System sends GET request to Salesforce with URL-encoded query
- Salesforce returns up to 2,000 records per page with `done: false` and `nextRecordsUrl` for additional pages
- When `done: false`, system automatically dispatches `ProcessFlowPage` job to fetch next page via `nextRecordsUrl`
- All pages aggregated into flow state for downstream nodes
- Final result set contains all records from all pages

**Response Schema**:
```json
{
  "totalSize": 5500,
  "done": false,
  "nextRecordsUrl": "/services/data/v59.0/query/01gxx-2000",
  "records": [
    {
      "attributes": {"type": "Contact", "url": "/services/data/v59.0/sobjects/Contact/003..."},
      "Id": "003xx000004TmiXABC",
      "Name": "John Doe"
    }
  ]
}
```

---

### 3. Create Records (POST)

**Endpoint**: `POST /services/data/v59.0/sobjects/{object}/`
**Purpose**: Create new Salesforce records from flow payloads.

**Behavior**:
- API Node sends JSON body with field values: `{"FirstName": "Jane", "LastName": "Smith", "Email": "jane@test.com"}`
- Salesforce validates required fields and data types
- Returns HTTP 201 Created with new record ID on success
- Record ID extracted and available to downstream nodes as `{{payload.id}}`

**Response Schema**:
```json
{
  "id": "003xx000004TmiYABC",
  "success": true,
  "errors": []
}
```

---

### 4. Read Records (GET)

**Endpoint**: `GET /services/data/v59.0/sobjects/{object}/{id}`
**Purpose**: Fetch current state of a Salesforce record by ID.

**Behavior**:
- API Node specifies record type and ID in URL
- Returns all fields for the record
- Used for verification or conditional logic in flows

**Response Schema**: Standard Salesforce record JSON with all fields.

---

### 5. Update/Upsert Records (PATCH)

**Endpoints**:
- Update: `PATCH /services/data/v59.0/sobjects/{object}/{id}`
- Upsert: `PATCH /services/data/v59.0/sobjects/{object}/{externalIdField}/{externalIdValue}`

**Purpose**: Modify existing records or upsert by external ID.

**Behavior**:
- Update: Modifies specified fields only; unspecified fields unchanged. Returns HTTP 204 No Content.
- Upsert: If record with external ID exists, updates it (HTTP 200). If not, creates new record (HTTP 201).
- External ID upsert prevents duplicates when syncing from external systems

---

### 6. Automatic Token Refresh, Rate Limiting, Error Handling

**Token Refresh**:
- When Salesforce returns `INVALID_SESSION_ID` error, system automatically calls token refresh endpoint
- New access token stored in `connector_config.access_token`
- Original request retried with new token
- Maximum 3 retry attempts

**Rate Limiting**:
- Default throttling: 100 requests per 20 seconds (configurable via `connector.throttle_max_requests`)
- `ThrottleManager` tracks requests per connector
- When limit reached, delays subsequent requests until window resets
- If Salesforce returns `REQUEST_LIMIT_EXCEEDED`, exponential backoff (2s, 4s, 8s) with max 3 retries

**Error Handling**:
- Salesforce error codes mapped to iPaaS error patterns:
  - `INVALID_SESSION_ID` → Token refresh
  - `QUERY_TIMEOUT` → Log warning, no retry
  - `REQUEST_LIMIT_EXCEEDED` → Exponential backoff retry
  - `MALFORMED_QUERY` → Fail immediately with validation error
  - `REQUIRED_FIELD_MISSING` → Fail with validation error
  - `INVALID_FIELD` → Fail with validation error
- All errors logged with connector ID, flow ID, request context

---

## Architectural Boundaries

### Database Schema

**Existing Table**: `connector_config`
**Required Change**: Add `instance_url VARCHAR(255) NULL` column

**Migration**:
```sql
-- File: 2026_04_01_add_instance_url_to_connector_config.php
ALTER TABLE connector_config ADD COLUMN instance_url VARCHAR(255) NULL AFTER access_token_url;
```

**Seed Data**:
```sql
INSERT INTO connector_config_types (category, code, name, created_at, updated_at) VALUES
('application', 'salesforce_oauth2', 'Salesforce Production OAuth2', NOW(), NOW()),
('application', 'salesforce_sandbox_oauth2', 'Salesforce Sandbox OAuth2', NOW(), NOW());
```

### Framework Constraints

- **Laravel 10.x**: Must use existing `league/oauth2-client` v2.x for OAuth
- **Strategy Pattern**: Must extend `ConnectorStrategy` abstract class; no interface changes
- **Job Infrastructure**: Salesforce requests execute within existing `ProcessNode` timeout (300s)
- **Memory Limits**: Large result sets must not exceed PHP memory limit (256MB per job)
- **Pagination**: Must use existing `ProcessFlowPage` job for multi-page results

### Required Services (Must Use)

- `IpaasHelper::executeCurl()` for all HTTP requests (60s timeout default)
- `ThrottleManager` for rate limiting
- `AuthContext` for strategy selection
- `FlowCounterService` for job completion tracking
- `HasEncryptedFields` trait for token encryption

### Cannot Modify

- `ApiNode::execute()` signature or core logic
- `ProcessNode` job queue configuration
- `ConnectorStrategyInterface` method signatures
- `ProcessFlow` job structure

### Multi-Tenancy

- All connectors scoped to `tenant_connection` database
- Tokens encrypted using existing `HasEncryptedFields` trait
- Instance URLs stored in plaintext (not sensitive)
- No cross-tenant credential access

---

## Operational Dependencies

### Cloud Infrastructure

**OAuth Callback URL**: DevOps must configure route in production nginx
- **URL**: `https://app.suitex.com/tenant/ipaas/connector/oauth/callback`
- **Risk**: OAuth flow fails if route not configured

**Customer Setup**: Customers must create Salesforce Connected App
- **Required Scopes**: `api`, `refresh_token`, `offline_access`
- **Documentation**: Customer setup guide required for Connected App creation
- **Support Training**: Support team training on Salesforce Connected App troubleshooting

### Redis

- **Tenant Isolation Required**: All Redis keys MUST include tenant database identifier to prevent cross-tenant data contamination
- Pagination state pattern: `tenant:{tenantDatabase}:flow:{flowId}:run:{runId}:page:{pageNum}`
- Flow counter keys: `tenant:{tenantDatabase}:flow:{flowId}:run:{runId}:jobs_expected` / `jobs_completed`
- **Note**: Existing iPaaS Redis keys lack tenant isolation (identified bug). Salesforce implementation must follow correct pattern.
- No Redis configuration changes required
- Estimated memory: 50-100KB per active Salesforce flow per tenant

### Queue Configuration

- Uses existing `ipaas` queue (priority: normal)
- Estimated throughput: 100-200 Salesforce jobs/hour/tenant
- Existing capacity (500 jobs/hour) sufficient

### Monitoring

**Required Metrics** (via FlowMetricsService):
- Salesforce API call count per connector per hour
- Average response time for queries
- Token refresh failure rate
- Rate limit hit count

**Alert Thresholds**:
- Token refresh failure >5% → Slack alert to #ipaas-alerts
- Rate limit hits >10/hour/connector → Email to admin
- Query timeout >2% → PagerDuty alert

---

## Acceptance Criteria

### OAuth2 Authentication
- [ ] User can create Salesforce Production connector, redirect to `login.salesforce.com`, authorize, and connector shows "Active"
- [ ] User can create Salesforce Sandbox connector, redirect to `test.salesforce.com`, authorize, and connector shows "Active"
- [ ] Instance URL extracted from OAuth response and stored in `connector_config.instance_url`
- [ ] Access token and refresh token encrypted in database
- [ ] Invalid Client ID returns error: "Authentication failed: invalid_client_id"
- [ ] User denies authorization returns error: "Authorization denied by user"

### SOQL Query Execution
- [ ] Query `SELECT Id, Name FROM Contact LIMIT 50` returns 50 records in preview panel
- [ ] Query result includes `totalSize`, `done`, and `records` array
- [ ] Query with syntax error returns: "SOQL Error: No such column 'InvalidField' on Contact"
- [ ] Query returning 5,500 records automatically paginates (3 API calls) and returns complete dataset
- [ ] Pagination tracked in flow execution logs with page numbers
- [ ] Each page fetched via `nextRecordsUrl` from previous response

### Record Create (POST)
- [ ] POST to `/sobjects/Contact/` with valid JSON creates record and returns Salesforce ID
- [ ] Record ID available to downstream nodes as `{{payload.id}}`
- [ ] Missing required field returns: "REQUIRED_FIELD_MISSING: LastName is required"
- [ ] Invalid field name returns: "INVALID_FIELD: No such column 'Emaill' on Contact"
- [ ] Created record visible in Salesforce UI with correct field values

### Record Read (GET)
- [ ] GET `/sobjects/Contact/{id}` returns full record with all fields
- [ ] Non-existent ID returns HTTP 404
- [ ] Response includes standard Salesforce record structure with `attributes`

### Record Update/Upsert (PATCH)
- [ ] PATCH `/sobjects/Contact/{id}` with `{"Email": "new@test.com"}` updates only Email field
- [ ] Update returns HTTP 204 No Content
- [ ] Unchanged fields remain intact after update
- [ ] PATCH `/sobjects/Contact/External_ID__c/EXT-123` creates record if not exists (HTTP 201)
- [ ] Second PATCH to same external ID updates existing record (HTTP 200), no duplicate created
- [ ] Upsert by external ID prevents duplicates when syncing from external systems

### Token Refresh
- [ ] Expired token (manually set `expires` to past) triggers automatic refresh on next API call
- [ ] Refresh token sent to Salesforce token endpoint
- [ ] New access token stored in database
- [ ] Original API request retried with new token
- [ ] Flow completes successfully without user intervention
- [ ] Logs show: "Token refresh initiated" → "Token refresh successful" → "API call successful"

### Rate Limiting
- [ ] Connector with 100 requests/20s limit delays 101st request until window resets
- [ ] Flow with 150 sequential requests completes in 40-45 seconds (not 15s) due to throttling
- [ ] No `REQUEST_LIMIT_EXCEEDED` errors from Salesforce
- [ ] ThrottleManager logs: "Request 100/100 in current window - throttling next request"
- [ ] Rate limit window reset allows requests to resume

### Error Handling
- [ ] `INVALID_SESSION_ID` triggers token refresh and retry (max 3 attempts)
- [ ] `QUERY_TIMEOUT` logs warning and fails flow with timeout error
- [ ] `MALFORMED_QUERY` fails immediately with syntax error message
- [ ] `REQUIRED_FIELD_MISSING` fails with validation error
- [ ] `INVALID_FIELD` fails with field name error
- [ ] All errors logged with connector ID, flow ID, error code, error message

### Multi-Environment Isolation
- [ ] Production connector queries `https://na50.salesforce.com`
- [ ] Sandbox connector queries `https://cs42.sandbox.salesforce.com`
- [ ] No cross-contamination between environments
- [ ] Each connector uses its own access token
- [ ] Database shows correct `instance_url` for each connector

### End-to-End Integration
- [ ] Flow: Salesforce Query → Transform → NetSuite Create executes successfully
- [ ] 100 Salesforce Contact records retrieved, transformed, and created in NetSuite
- [ ] Flow status: "Completed Successfully"
- [ ] Flow metrics show: 100 records processed, ~45s duration
- [ ] All Salesforce field mappings accurate in NetSuite

---

## Technical Notes

### Code Organization

**New Files** (~250 lines):
```
src/App/Services/Ipaas/Connectors/SalesforceService.php
```

**Modified Files**:
```
src/Domain/Ipaas/Connectors/Strategy/ConnectorOAuth2.php
```

**Test Files** (new):
```
tests/Unit/Services/Ipaas/Connectors/SalesforceServiceTest.php
tests/Unit/Domain/Ipaas/Connectors/Strategy/ConnectorOAuth2SalesforceTest.php
tests/Feature/Http/Controllers/Ipaas/SalesforceConnectorTest.php
```

### SalesforceService Implementation

**Key Methods**:
- `extractInstanceUrl($oauthResponse)` - Parse instance URL from OAuth callback
- `buildQueryUrl($instanceUrl, $soql)` - Construct query endpoint with URL encoding
- `formatQueryResponse($response)` - Normalize Salesforce response for flow processing
- `handlePaginationToken($response)` - Extract `nextRecordsUrl` for `ProcessFlowPage` dispatch
- `applySalesforceHeaders($headers)` - Add Authorization: Bearer {token} header
- `mapErrorCode($sfErrorCode)` - Map Salesforce error codes to iPaaS error patterns

### ConnectorOAuth2 Integration

Add to `makeRequest()` method:

```php
public function makeRequest($input)
{
    try {
        $this->setToken = true;

        $connector = $input['connector'];
        $applicationCode = $connector->app?->code ?? '';

        if (in_array($applicationCode, ['salesforce_oauth2', 'salesforce_sandbox_oauth2'])) {
            $input = $this->applySalesforceLogic($connector, $input);
        }

        return $this->request($input);
    } catch (Exception $e) {
        throw $e;
    }
}

private function applySalesforceLogic($connector, $input)
{
    $sfService = new SalesforceService();

    // Override base_url with instance_url from config
    $instanceUrl = $connector->config->instance_url ?? $connector->base_url;
    $input['base_url_override'] = $instanceUrl;

    // Apply Salesforce-specific headers and formatting
    return $sfService->prepareRequest($connector, $input);
}
```

Add to `authenticate()` method to extract instance URL:

```php
// After OAuth provider returns access token
if ($redirect === false) {
    $connectorConfig->access_token = $accessToken->getToken();
    $connectorConfig->expires = (string) $accessToken->getExpires();

    // NEW: Extract and store instance URL for Salesforce
    if (in_array($connector->app?->code, ['salesforce_oauth2', 'salesforce_sandbox_oauth2'])) {
        $values = $accessToken->getValues();
        $connectorConfig->instance_url = $values['instance_url'] ?? null;
    }

    $connectorConfig->save();
}
```

### Pagination Flow

1. API Node executes query, receives response with `done: false` and `nextRecordsUrl`
2. `ProcessFlow` job detects pagination needed
3. `ProcessFlowPage` job dispatched with `nextRecordsUrl` as URL override
4. Each page processed, records aggregated in Redis state
5. When `done: true`, pagination stops, full dataset available to downstream nodes

### Error Code Mapping

```php
private function mapErrorCode($salesforceError): string
{
    $errorCode = $salesforceError['errorCode'] ?? '';

    return match($errorCode) {
        'INVALID_SESSION_ID' => 'token_expired',
        'QUERY_TIMEOUT' => 'query_timeout',
        'REQUEST_LIMIT_EXCEEDED' => 'rate_limit',
        'MALFORMED_QUERY' => 'validation_error',
        'REQUIRED_FIELD_MISSING' => 'validation_error',
        'INVALID_FIELD' => 'validation_error',
        'INVALID_LOGIN_CREDENTIALS' => 'auth_error',
        default => 'api_error'
    };
}
```

---

## Out of Scope (Future Enhancements)

- Composite API (batch multiple operations)
- Bulk API for large data loads (>10K records)
- Streaming API for real-time events
- Metadata API for schema management
- Apex REST custom endpoints
- Reports and Dashboards API
- SOAP API support
- JWT Bearer Flow authentication
- Custom field mapping UI
- SOQL query builder with autocomplete
- Automatic API version upgrades

---

## API Reference

### Salesforce Endpoints Used

- **OAuth Token**: `POST /services/oauth2/token`
- **Query**: `GET /services/data/v59.0/query/?q={soql}`
- **Query More**: `GET /services/data/v59.0/query/{queryLocator}`
- **Create**: `POST /services/data/v59.0/sobjects/{object}/`
- **Read**: `GET /services/data/v59.0/sobjects/{object}/{id}`
- **Update**: `PATCH /services/data/v59.0/sobjects/{object}/{id}`
- **Delete**: `DELETE /services/data/v59.0/sobjects/{object}/{id}`
- **Upsert**: `PATCH /services/data/v59.0/sobjects/{object}/{externalField}/{externalId}`

### Common Salesforce Error Codes

| Error Code | Meaning | iPaaS Handling |
|-----------|---------|----------------|
| `INVALID_SESSION_ID` | Token expired | Refresh token, retry (max 3x) |
| `QUERY_TIMEOUT` | Query too complex | Log warning, fail flow |
| `REQUEST_LIMIT_EXCEEDED` | Rate limit hit | Exponential backoff, retry (max 3x) |
| `MALFORMED_QUERY` | SOQL syntax error | Fail immediately with validation error |
| `REQUIRED_FIELD_MISSING` | Missing required field | Fail with validation error |
| `INVALID_FIELD` | Field doesn't exist | Fail with validation error |
| `INVALID_LOGIN_CREDENTIALS` | Auth failed | Fail with auth error |

### Rate Limits (Salesforce Default)

- Standard Edition: 15,000 API calls per 24 hours
- Enterprise/Unlimited: 100,000 API calls per 24 hours
- Concurrent request limit: 25 requests at a time
- Query result pagination: 2,000 records per page

---

**Related Documentation**:
- Epic 7 design-spec (if applicable for integration patterns)
- iPaaS connector architecture overview
- OAuth2 strategy pattern documentation
