CRM Integration — Technical Documentation
Also known as: CRM Push, CRM 2-Way Sync, CRM Deal Import, Integration Dispatch Last updated: March 2026
Feature Owners
| Role | Owners |
|---|---|
| Product managers | Martina Tuco, Kristijan Todorov |
| IT project manager | Martin Davitkov |
| Lead developer | Milena Atanasovska |
| Stakeholders | Kristijan Todorov, Katarina, Joe |
Table of Contents
- Overview
- Architecture
- Repository Map
- Supported CRMs
- End-to-End Flows
- Key Entities & Data Model
- Services & Business Logic
- Queue & Background Processing
- Portal Configuration UI
- CRM Sync Trigger System
- Shared vs CRM-Specific Logic
- Configuration
- Integration Points
- Known Pain Points & Future Improvements
1. Overview
CRM Integration pushes prospect and campaign data from Sopro into client CRM systems and pulls deal data back for ROI reporting. It spans two repositories — sopro-sodastream (event-driven push processing, legacy MVC5 API) and sopro-sodastream-core (2-way sync/deal import, Portal OAuth UI, queue consumers, trigger dispatch).
The system supports 6 active CRM integrations (HubSpot, Salesforce, Pipedrive, Zoho, Zapier, Worldpay) and has 3 placeholder/dead entries (Dynamics, Podio, Sugar) in the enum with no implementation.
Three subsystems cooperate:
| Subsystem | Repo | Purpose |
|---|---|---|
| Event-Driven Push | sopro-sodastream | Push prospect data TO client CRMs when campaign events fire (email delivered, response classified, website visit) |
| 2-Way Sync / Deal Import | sopro-sodastream-core | Pull deals FROM client CRMs, match to Sopro prospects, store for ROI reporting |
| CRM Sync Triggers | Both repos | Fire CRM push events from newer sources (webchat, web intent, SDR, LinkedIn, gifting) |
2. Architecture
3. Repository Map
sopro-sodastream (Legacy MVC5 API)
| Area | Key Files | Purpose |
|---|---|---|
| Strategy Interface | SoPro.API/Integrations/SoProIntegrations/ISoproIntegration.cs | Event contract: OnDelivered, OnResponded, OnClicked, OnWebsiteVisit, OnCrmSyncTrigger |
| Base Class | SoPro.API/Integrations/SoProIntegrations/SoproIntegrationBase.cs | Abstract base with no-op defaults for all hooks |
| Factory | SoPro.API/DispatcherHelpers/Helpers.cs | GetIntegrationInstance() — switch on Integration.Name string, manually news up implementations |
| Dispatcher | SoPro.API/Controllers/Dispatcher/EventDispatcherController.cs | HTTP POST endpoints receiving pipeline events, routes to CRM implementations |
| HubSpot Push | SoPro.API/Integrations/Implementations/HubSpot/HubSpotIntegration.cs | ~506 LOC, creates companies/contacts/deals via HubSpot REST API |
| Salesforce Push | SoPro.API/Integrations/Implementations/Salesforce/SalesforceIntegration.cs | ~631 LOC, uses Salesforce.Common/Salesforce.Force NuGet packages |
| Pipedrive Push | SoPro.API/Integrations/Implementations/PipeDrive/PipeDriveIntegration.cs | ~309 LOC, token-based auth, custom field-key mapping |
| Zoho Push | SoPro.API/Integrations/Implementations/Zoho/ZohoIntegration.cs | ~480 LOC, creates both Leads AND Contacts, domain-aware multi-region API |
| Zapier Push | SoPro.API/Integrations/Implementations/Zapier/ZapierIntegration.cs | ~135 LOC, webhook-based, no direct CRM API |
| Worldpay Push | SoPro.API/Integrations/Implementations/Worldpay/WorldpayIntegration.cs | ~208 LOC, largely commented out |
| Helpers | Services/Integrations/Helpers/CRMHelpers.cs | Email content reading from Azure Storage, company size normalization, industry mapping |
| Entities | SoProEntities/Integration.cs, IntegrationCampaign.cs, IntegrationObject.cs, IntegrationError.cs | Core integration data model |
| Enums | SoProEntities/Enums/IntegrationEnum.cs | All 9 CRM types with DB-matching integer values |
| API Wrappers | Services/Integrations/HubSpot/, Salesforce/, PipeDrive/, Zoho/ | Per-CRM REST client wrappers |
sopro-sodastream-core (ASP.NET Core)
| Area | Key Files | Purpose |
|---|---|---|
| Import Services | CRMServices/HubspotImportService.cs, PipedriveImportService.cs, SalesforceImportService.cs, ZohoImportService.cs | Pull deals from CRM APIs, match to Sopro prospects |
| Shared Deal Service | CRMServices/CRMDealService.cs | Persists imported deals, prospects, companies, pipelines via Dapper |
| Queue Consumer | SoProQueueConsumer/Partials/TaskCRMTwoWaySyncHubspot.cs | Dispatches to all 4 CRM import services (misleadingly named) |
| Batch Processing | SoProQueueConsumer/Partials/TaskIntegrationProcessing.cs | Processes backdate batch items from IntegrationScheduledProcessing |
| Trigger Service | DapperSopro/Services/CRM/DapperCrmSyncTriggerService.cs | HTTP client that POSTs events to EventDispatcher endpoints |
| Portal Controllers | Portal.Web/Controllers/HubSpotIntegrationController.cs, SalesforceIntegrationController.cs, PipedriveIntegrationController.cs, ZohoIntegrationController.cs | OAuth flows and settings wizards per CRM |
| Portal Listing | Portal.Web/Controllers/IntegrationsController.cs | Main integrations page, IntegrationSettings deserializes per-CRM config |
| CRM Entities | SoProEntities/CRMSync.cs, CRMSyncLog.cs, CRMDealImport.cs, CRMDealProspect.cs, CRMDealCompany.cs, CRMDealPipeline.cs, CRMDealPipelineStage.cs | 2-way sync data model |
| Batch Entities | SoProEntities/IntegrationScheduledProcessing.cs, IntegrationScheduledProcessingBatch.cs | Backdate batch tracking |
| Enums (Dapper) | DapperSopro/Enums/CRM/IntegrationEnum.cs | 4 CRMs only (HubSpot, Pipedrive, Salesforce, Zoho) — auto-incremented from 0, does NOT match legacy enum values |
| Trigger Enum | DapperSopro/Enums/CRM/CRMSyncTriggerTypeEnum.cs | 27 trigger types |
| OAuth Configs | CRMServices/Configs/HubSpotConfig.cs | Hardcoded ClientId/Secret (HubSpot) |
4. Supported CRMs
IntegrationEnum (canonical — sopro-sodastream)
SoProEntities/Enums/IntegrationEnum.cs:
| CRM | Enum Value | Push | Pull (2-Way) | Portal OAuth | Status |
|---|---|---|---|---|---|
| HubSpot | 1 | Yes | Yes | HubSpotIntegrationController | Active |
| Dynamics | 2 | — | — | — | Placeholder (no code) |
| Podio | 3 | — | — | — | Placeholder (no code) |
| Zoho | 4 | Yes | Yes | ZohoIntegrationController | Active |
| Sugar | 5 | — | — | — | Placeholder (no code) |
| Salesforce | 6 | Yes | Yes | SalesforceIntegrationController | Active |
| Pipedrive | 7 | Yes | Yes | PipedriveIntegrationController | Active |
| Worldpay | 72 | Partial (commented out) | — | — | Legacy/Dead |
| Zapier | 78 | Yes (webhook) | — | — | Active (push-only) |
Dapper IntegrationEnum (sopro-sodastream-core)
DapperSopro/Enums/CRM/IntegrationEnum.cs — WARNING: Uses auto-increment (0, 1, 2, 3) which does not match the legacy DB values. Only 4 CRMs: HubSpot, Pipedrive, Salesforce, Zoho.
TaskCRMTwoWaySyncHubspot Integration IDs
The queue consumer uses hardcoded integration IDs to look up campaigns: HubSpot=1, Pipedrive=7, Salesforce=32, Zoho=35. These are IntegrationCampaign.Id values (row IDs), not IntegrationEnum values.
5. End-to-End Flows
5.1 Event-Driven Push Flow
Dual processing path: Each event (sent, delivered, responded, etc.) checks IntegrationCampaign.ProccessOnConsoleApplication:
false— processes inline viaProcessorclasstrue— creates anIntegrationScheduledProcessingrecord and enqueues to the CRM-specific queue for batch processing
5.2 Deal Import / 2-Way Sync Flow
5.3 CRM Sync Trigger Flow
5.4 Portal OAuth Setup Flow
6. Key Entities & Data Model
Push-Side Entities (sopro-sodastream)
| Table / Entity | Key Columns | Purpose |
|---|---|---|
Integration | Id, Name, ClientId, Secret | Master CRM definition (OAuth credentials, display info) |
IntegrationCampaign | Id, CampaignId, IntegrationId, JsonToken, JsonUserConfig, ProccessOnConsoleApplication, NewTriggers | Per-campaign CRM configuration (tokens, field mappings, processing mode) |
IntegrationObject | [IntegrationId, ProspectEmailId] (composite PK), CrmProspectId, CrmCompanyId, CrmDealId | Tracks CRM-side IDs for each synced prospect |
IntegrationError | IntegrationId, CampaignId, Stage, Count | Aggregated error tracking per integration/campaign |
IntegrationObjectLog | FK → IntegrationObject | Audit trail of integration object changes |
Import-Side Entities (sopro-sodastream-core)
| Table / Entity | Key Columns | Purpose |
|---|---|---|
CRMSync | Id, IntegrationId, CampaignId, ProspectId, CRMSyncTriggerTypeId, CrmProspectId, CrmCompanyId, CrmDealId | Audit record per CRM push event |
CRMSyncLog | CRMSyncId, LogData | Detailed log per CRM sync |
CRMSyncTriggerType | Id, Description, Priority | Lookup table for 27 trigger types |
CRMDealImport | Id, IntegrationId, CampaignId, CrmDealId, CrmDealName, CrmDealAmount, CrmPipelineId, SoproProspectId, SoproMatchType | Imported deals from client CRMs |
CRMDealImportLog | Same shape (audit copy) | Historical snapshot of each import run |
CRMDealImportStat | IntegrationId, CampaignId, StepNumber, RowsCount, DurationInSeconds | Per-step import statistics |
CRMDealProspect | CrmDealId, CrmProspectId, SoproProspectId, Email, DetailsFrom | CRM contact → Sopro prospect match |
CRMDealCompany | CrmDealId, CrmCompanyId, SoproCompanyId, CompanyDomain, DetailsFrom | CRM company → Sopro company match |
CRMDealPipeline | CrmPipelineId, CrmPipelineLabel | CRM pipeline definitions |
CRMDealPipelineStage | CrmPipelineStageId, CrmPipelineStageLabel, CrmPipelineStageProbability | Pipeline stage definitions |
Batch Processing Entities
| Table / Entity | Key Columns | Purpose |
|---|---|---|
IntegrationScheduledProcessing | Id, IntegrationId, ProspectId, CampaignId, Status, Priority, Action, CrmSyncTriggerType | One queue item per prospect to process |
IntegrationScheduledProcessingBatch | Id, CampaignId, IntegrationId, TotalIntegrations, ProcessedIntegrations, FailedIntegrations | Batch header tracking progress |
IntegrationProcessingStatusEnum: Pending = 0, Processing = 1, Processed = 2, Failed = 3
7. Services & Business Logic
Push Integration Factory (sopro-sodastream)
SoPro.API/DispatcherHelpers/Helpers.cs:
public ISoproIntegration GetIntegrationInstance(IntegrationCampaign ic)
{
switch (ic.Integration.Name)
{
case "HubSpot": return new HubSpotIntegration(...);
case "Pipedrive": return new PipedriveIntegration(...);
case "Salesforce": return new SalesforceIntegration(...);
case "Worldpay": return new WorldpayIntegration(...);
case "Zoho": return new ZohoIntegration(...);
case "Zapier": return new ZapierIntegration(...);
default: throw new Exception("Integration not found");
}
}
No DI — implementations are manually constructed. String-based routing via Integration.Name, not the IntegrationEnum.
ISoproIntegration Interface
SoPro.API/Integrations/SoProIntegrations/ISoproIntegration.cs:
| Method | When Called | Typical Action |
|---|---|---|
OnAdded | Campaign created | No-op for most CRMs |
OnSent | Email sent | Create company (no contacts) |
OnSentBatch | Batch email send | Create companies + contacts |
OnDelivered | Email delivered | Get-or-create Company → Contact → save IntegrationObject |
OnOpened | Email opened | No-op for most CRMs |
OnClicked | Link clicked | No-op for most CRMs |
OnResponded | Response classified | Same as delivered + Create Deal (stage based on response type) |
OnWebsiteVisit | Tracked website visit | No-op or create record depending on CRM |
OnCrmSyncTrigger | New trigger system | Map trigger type → stage/status, create or update CRM records |
Import/Sync Services (sopro-sodastream-core)
Each CRM has an independent import service with no shared interface:
| Service | File | Methods |
|---|---|---|
IHubspotImportService | CRMServices/HubspotImportService.cs | CRM_ImportDeals(), RefreshToken() |
ISalesforceImportService | CRMServices/SalesforceImportService.cs | CRM_ImportDeals(), RefreshToken() |
IPipedriveImportService | CRMServices/PipedriveImportService.cs | CRM_ImportDeals(), RefreshToken() |
IZohoImportService | CRMServices/ZohoImportService.cs | CRM_ImportDeals(), RefreshToken() |
All four follow the same internal pattern:
- Refresh OAuth token
- Fetch all deals and pipelines from CRM API
- Upsert to
CRMDealImporttable - Run stored procedures to match CRM contacts/companies to Sopro data
- Generate stats and logs
Shared CRMDealService
ICRMDealService in CRMServices/Interfaces/ICRMDealService.cs provides shared persistence:
Task InsertUpdateDeals(List<DapperCRMDealImport> deals, DateTime date);
Task<List<DapperCRMDealProspect>> GetMatchedProspects(int integId, int campId, int clientId);
Task<List<DapperCRMDealCompany>> GetMatchedCompanies(int integId, int campId, int clientId);
Task InsertUpdatePipelines(List<DapperCRMDealPipeline> pipelines, DateTime date);
Task InsertUpdateStages(List<DapperCRMDealPipelineStage> stages, DateTime date);
Task ExecSP_CRMDealsImport_Step2_MatchCrmDataWithSoproDb(...);
Task ExecSP_CRMDealsImport_Step5_MatchCrmDataWithSoproDb(...);
Registered as AddTransient<ICRMDealService, CRMDealService>() in SoProQueueConsumer/InitServices.cs.
8. Queue & Background Processing
Azure Queues
| Queue (ConsoleTypeEnum) | Int | Queue Name Pattern | Purpose |
|---|---|---|---|
IntegrationHub | 9 | integrationhub | HubSpot scheduled push processing |
IntegrationSalesForce | 10 | integrationsalesforce | Salesforce scheduled push processing |
IntegrationPipedrive | 11 | integrationpipedrive | Pipedrive scheduled push processing |
IntegrationZoho | 12 | integrationzoho | Zoho scheduled push processing |
IntegrationZapier | 27 | integrationzapier | Zapier scheduled push processing |
CrmTwoWaySyncHubspot | 41 | crm-2-way-sync-hubspot | 2-way sync for ALL 4 CRMs (not just HubSpot) |
Queue Consumer Tasks
| Task File | Queue | Behavior |
|---|---|---|
TaskCRMTwoWaySyncHubspot.cs | CrmTwoWaySyncHubspot (41) | Checks for HubSpot (ID=1), Pipedrive (ID=7), Salesforce (ID=32), Zoho (ID=35) integrations on the campaign, calls each CRM_ImportDeals() |
TaskIntegrationProcessing.cs | Per-CRM queues (9–12, 27) | Processes individual IntegrationScheduledProcessing items from backdate batches |
Batch Processing Model
For backdate operations (pushing historical data to CRMs):
IntegrationScheduledProcessingBatchis created with total count- One
IntegrationScheduledProcessingrecord per prospect (status:Pending) - Items are enqueued to CRM-specific queue
- Queue consumer processes each item, updates status to
ProcessedorFailed - Batch tracks
ProcessedIntegrationsandFailedIntegrationscounts
9. Portal Configuration UI
OAuth Flow
Each CRM has a dedicated Portal controller (Portal.Web/Controllers/) handling:
| Step | Controller Action | Description |
|---|---|---|
| 1. Initiate | Authorize(campaignId) / GoToZoho(campaignId, domain) | Generates OAuth URL, stores campaignId in session |
| 2. Callback | Token(code) / Token(code, state) | Exchanges auth code for tokens, fetches CRM pipelines/fields |
| 3. Wizard | Returns _AuthWizard.cshtml partial | Renders field mapping, pipeline selection, stage mapping UI |
| 4. Submit | Submit(userSettings, token, ...) | Saves to IntegrationCampaign.JsonToken + IntegrationCampaign.JsonUserConfig |
| 5. Edit | EditUserSettings(id) | Loads existing config, refreshes CRM metadata, shows edit wizard |
| 6. Delete | Delete(id) | Removes integration, logs to IntegrationCampaignHistoryLog |
Settings Wizard per CRM
The IntegrationSettings partial views (_HubspotDetails.cshtml, _SalesforceDetails.cshtml, etc.) are loaded by IntegrationsController.IntegrationSettings() via a switch on Integration.Name.
UserSettings / Field Mapping JSON
Stored in IntegrationCampaign.JsonUserConfig. Common settings across all CRMs:
| Setting | Type | Purpose |
|---|---|---|
ImportOnSend | bool | Sync on email delivery |
addOpportunities | bool | Create deals/opportunities |
stage{TriggerType} | string | Deal stage ID when trigger fires |
stage{TriggerType}Name | string | Deal stage display name |
leadStatus{TriggerType} | string | Lead status per trigger type (18 variants each) |
sourceKey / sourceValue | string | Source attribution for created CRM records |
mapping | Dictionary<string, Dictionary<string, string>> | Nested field mapping: category → (Sopro field → CRM field) |
Mapping categories: "company", "companyText", "contact", "contactText", "leads", "leadsText", "deals", "dealsText"
CRM-specific settings vary — e.g., HubSpot has lifecycleStage, initialLeadStatus, addTimelineActivity; Salesforce has leadStatus, recordType, requiredFields, sandbox flag; Zoho has dealClosingDateDays, domain, useCompanyMapping.
10. CRM Sync Trigger System
CRMSyncTriggerTypeEnum (27 trigger types)
DapperSopro/Enums/CRM/CRMSyncTriggerTypeEnum.cs:
| Category | Trigger | Value |
|---|---|---|
| Legacy Response | PositiveResponse | 2 |
Referral | 3 | |
DeferredInterestKeepinTouch | 4 | |
PoliteDecline | 5 | |
EmailDelivered | 100 | |
| Web Intent | WebsiteVisit | 110 |
WebIntentLead | 130 | |
MultipleVisits | 180 | |
ReturningInterest | 181 | |
GoalUrl | 182 | |
| Webchat | WebchatLead | 120 |
WebchatSession | 190 | |
InboundWebchat | 201 | |
PositiveResponseLinkedIn | 170 | |
ReferralLinkedIn | 171 | |
DeferredInterestLinkedIn | 172 | |
PoliteDeclineLinkedIn | 173 | |
ActiveDialogueLinkedIn | 174 | |
| SDR | CalledPitchFollowUpBooked | 200 |
InboundWebform | 202 | |
InboundCallPositive | 203 | |
| Other | GiftRedemptionLead | 140 |
ExitIntentFormSubmissionLead | 150 | |
ManualEventLeadOrSDRLead | 160 | |
PhoneNumber | 210 | |
| Fallback | Unknown | 255 |
Trigger Sources
| Service | Repo | Triggers Fired |
|---|---|---|
SoproChatController | sopro-sodastream-core | WebchatSession, InboundWebchat |
SoproChatActivityService | sopro-sodastream | InboundWebchat, WebchatSession |
SalesDevelopmentService | sopro-sodastream | InboundWebform, InboundCallPositive, InboundWebchat |
ProspectIntentTrackingService | sopro-sodastream | ReturningInterest, MultipleVisits, GoalUrl |
WebsiteWidgetService | sopro-sodastream | WebsiteVisit |
ResponderMessageImapService | sopro-sodastream | Response-based triggers (mapped from responder type) |
Old vs New Trigger Split
IntegrationCampaign.NewTriggers (bool) determines which trigger path is used:
false(legacy): Events are mapped fromResponderTypeenum valuestrue(new): Events useCRMSyncTriggerTypeEnumwith per-trigger stage/status mapping
Both paths must be maintained — every CRM implementation contains branching logic for both.
11. Shared vs CRM-Specific Logic
Shared Across All CRMs
| Component | Location | Notes |
|---|---|---|
ISoproIntegration + SoproIntegrationBase | sopro-sodastream | Strategy contract with no-op defaults |
IntegrationObject tracking | sopro-sodastream | All CRMs store CrmProspectId, CrmCompanyId, CrmDealId |
CRMSync + CRMSyncLog audit | sopro-sodastream-core | Shared audit entities |
CRMDealService | sopro-sodastream-core | All import services use this for persistence |
| Stored procedures (Steps 2, 5, 7) | Database | CRM-agnostic matching logic |
IntegrationScheduledProcessing batch | sopro-sodastream-core | Same batch framework for all CRMs |
UserSettings base fields | Both repos | ImportOnSend, addOpportunities, trigger-stage/status mappings |
CRM-Specific Differences
| Aspect | HubSpot | Salesforce | Pipedrive | Zoho | Zapier |
|---|---|---|---|---|---|
| Auth | OAuth2 (hardcoded ClientId/Secret in HubSpotConfig.cs) | OAuth2 with sandbox/production toggle | API token (not OAuth) | OAuth2 with multi-region domain | None (webhook URLs) |
| CRM objects created | Companies, Contacts, Deals, Engagements, Timeline Activities | Accounts, Contacts, Leads, Opportunities, Tasks | Organizations, Persons, Deals | Accounts, Contacts, Leads, Deals | Webhook payloads only |
| Contact model | Contacts only | Contacts AND Leads | Persons | Contacts AND Leads | N/A |
| Pipeline handling | Per-trigger pipeline + stage mapping | Stages only, no per-trigger pipeline | Stages only | Stages only | N/A |
| Owner mapping | Email → HubSpot Owner ID lookup | Salesforce User lookup | Owner lookup | Zoho User lookup | N/A |
| Field mapping UI | Table with CRM field dropdowns | Same | Same (with custom field-key mapping) | Same | N/A |
| Token refresh | HubSpotWrapper.RefreshToken() | AuthenticationClient.TokenRefreshAsync() | Reuses HubSpotWrapper.RefreshToken() | ZohoWrapper.RefreshToken() | N/A |
| Pull/Import | Full deal import with pipeline stages | Opportunity import | Deal import | Deal import | None |
| Unique behaviors | addTimelineActivity, lifecycleStage | Sandbox flag, recordType, requiredFields | Custom field-key system | useCompanyMapping, dealClosingDateDays, multi-domain | Webhook subscriptions list in JsonToken |
Duplicated Code
| Code | Copies Found | Locations |
|---|---|---|
CRMHelpers | 2 | sopro-sodastream/Services/Integrations/Helpers/CRMHelpers.cs (~313 LOC), sopro-sodastream-core/Portal.Services/DTOs/Integrations/Helpers/CRMHelpers.cs (~310 LOC) |
CRMSyncTriggerTypeEnum | 2+ | DapperSopro/Enums/CRM/, Portal.Services/Enums/ |
IntegrationEnum | 3 | sopro-sodastream (9 values), sopro-sodastream-core Dapper (4 values, mismatched IDs), sopro-sodastream-core SoProEntities (copy of legacy) |
| Token refresh pattern | 4 | Each *ImportService copy-pastes the same RefreshTokenResult logic |
| UserSettings class | 3+ | Per CRM, per project (Portal.Services, DapperSopro, CRMServices) |
12. Configuration
IntegrationCampaign Key Fields
| Field | Type | Purpose |
|---|---|---|
JsonToken | string (JSON) | Serialized OAuth tokens — refreshed on each use |
JsonUserConfig | string (JSON) | Serialized user settings (field mappings, stages, pipelines) |
RefreshToken | string | Stored separately for easy access |
ProccessOnConsoleApplication | bool | false = inline push, true = queued batch push |
NewTriggers | bool | false = legacy ResponderType mapping, true = CRMSyncTriggerTypeEnum |
EnableBackDateFromClient | bool | Allow backdating historical data to CRM |
OAuth Credentials
| CRM | Storage | Notes |
|---|---|---|
| HubSpot | Hardcoded in CRMServices/Configs/HubSpotConfig.cs | Multiple commented-out values for different environments |
| Salesforce | Integration.ClientId + Integration.Secret (DB) | Has sandbox toggle per campaign |
| Zoho | Integration.ClientId + Integration.Secret (DB) | Domain-specific |
| Pipedrive | API token stored in IntegrationCampaign.JsonToken | Not OAuth2, simpler token model |
Queue Processing Config
| Setting | Source | Purpose |
|---|---|---|
APIWebsite | GeneralSettings (DB) | URL for DapperCrmSyncTriggerService to POST to EventDispatcher |
| Queue names | ConsoleTypeEnum | Azure queue routing for each CRM |
13. Integration Points
Cross-Repo Communication
| From | To | Mechanism | Details |
|---|---|---|---|
| sopro-sodastream-core trigger sources | sopro-sodastream EventDispatcher | HTTP POST | DapperCrmSyncTriggerService → /EventDispatcher/OnCrmSyncTrigger/ |
| sopro-sodastream-core trigger sources | sopro-sodastream EventDispatcher | HTTP POST | DapperCrmSyncTriggerService → /EventDispatcher/OnWebhookCrmSyncTrigger/ |
| sopro-sodastream EventDispatcher | Azure queues | Azure Queue | Enqueues IntegrationScheduledProcessing to per-CRM queues (types 9–12, 27) |
| Azure queue (type 41) | sopro-sodastream-core TaskCRMTwoWaySyncHubspot | Azure Queue | Campaign ID message triggers 2-way sync |
| sopro-sodastream push implementations | External CRM APIs | REST | HubSpot, Salesforce, Pipedrive, Zoho, Zapier webhook URLs |
| sopro-sodastream-core import services | External CRM APIs | REST | Pull deals, pipelines, contacts, companies |
| sopro-sodastream-core Portal | External CRM OAuth servers | OAuth2 | Token exchange and refresh |
Shared Database Tables
Both repos read/write to the same SQL database:
Integration,IntegrationCampaign,IntegrationObject— used by push (sopro-sodastream) and Portal (sopro-sodastream-core)CRMSync,CRMSyncLog— written by push, read for reportingCRMDealImport,CRMDealProspect,CRMDealCompany,CRMDealPipeline,CRMDealPipelineStage— written by import servicesIntegrationScheduledProcessing,IntegrationScheduledProcessingBatch— written by EventDispatcher, processed by queue consumer
14. Known Pain Points & Future Improvements
14.1 Architectural Issues
| Issue | Impact | Details |
|---|---|---|
| Massive code duplication | Adding a new CRM requires copy-pasting thousands of LOC | CRMHelpers duplicated across repos, UserSettings defined 3+ times per CRM, token refresh logic copy-pasted in 4 import services, trigger-type stage mapping repeated across all implementations |
| Manual factory with no DI | Untestable, fragile | Helpers.GetIntegrationInstance() uses new with ~12 constructor parameters, string-based routing via Integration.Name |
| Split across two repos | Feature changes touch both repos | Push logic in sopro-sodastream (MVC5), pull/import in sopro-sodastream-core (ASP.NET Core), Portal UI in sopro-sodastream-core |
| 4000+ LOC integration classes | Hard to maintain, test, or extend | Each CRM pushes all event handling into a single massive class |
| Inconsistent enum definitions | Risk of ID mismatches | Dapper IntegrationEnum auto-increments from 0, legacy enum uses DB-matching IDs (1, 2, 4, 6, 7, 72, 78) |
| No shared import interface | Each new CRM needs bespoke dispatch code | TaskCRMTwoWaySyncHubspot hardcodes the integration IDs and individually calls each import service |
| Old vs New trigger split | Permanent branching complexity | NewTriggers flag means every CRM must handle both ResponderType and CRMSyncTriggerTypeEnum paths |
Heavy use of dynamic | Runtime failures, no compile-time safety | HubSpot, Pipedrive, and Zoho extensively deserialize API responses as dynamic |
14.2 Specific Code Smells
| Smell | Location | Details |
|---|---|---|
| Hardcoded OAuth secrets | CRMServices/Configs/HubSpotConfig.cs | ClientId and Secret as string constants with commented-out staging values |
| Client-specific hacks | EventDispatcher, Zoho integration | campaign.ClientId == 2806 checks, hardcoded email-to-owner mappings |
| Misleading naming | TaskCRMTwoWaySyncHubspot | Processes ALL 4 CRMs, not just HubSpot |
| Commented-out code | WorldpayIntegration.OnDelivered | Entire method body commented out, class still active |
| Pipedrive reuses HubSpot token refresh | PipedriveImportService.RefreshToken() | Calls HubSpotWrapper.RefreshToken() for Pipedrive tokens |
14.3 Recommended Future State
To support adding 1–10 new CRMs with minimal development:
1. Unified CRM Adapter Interface
Create a single interface unifying push and pull operations so each CRM only needs to implement one adapter:
public interface ICrmAdapter
{
Task<CrmCompany> GetOrCreateCompany(ProspectData data);
Task<CrmContact> GetOrCreateContact(ProspectData data);
Task<CrmDeal> CreateDeal(DealData data);
Task<List<CrmDeal>> ImportDeals(int campaignId);
Task RefreshToken(IntegrationCampaign config);
}
2. Plugin-Based Registration
Replace the manual switch factory with DI-resolved keyed services:
services.AddKeyedScoped<ICrmAdapter, HubSpotAdapter>("HubSpot");
services.AddKeyedScoped<ICrmAdapter, SalesforceAdapter>("Salesforce");
3. Centralize Shared Logic
- Single
CRMHelpersin a shared library - Single
IntegrationEnumwith DB-matching values used everywhere - Base class for token refresh with CRM-specific override for the HTTP call only
- Generic field mapping engine instead of per-CRM
UserSettingsclasses
4. Consolidate to One Repo
Move push logic from sopro-sodastream to sopro-sodastream-core to eliminate the cross-repo HTTP dependency (DapperCrmSyncTriggerService → EventDispatcher).
5. Strongly-Typed API Models
Replace dynamic deserialization with typed DTOs for each CRM's API responses. This provides compile-time safety and makes CRM API version changes detectable at build time.
6. Configuration-Driven CRM Setup
Move from code-based CRM setup to configuration-driven setup where a new CRM can be added via:
- CRM metadata config (auth type, API base URL, entity mappings)
- Field mapping templates
- Pipeline/stage configuration schema
- Webhook vs OAuth flag
7. Eliminate the Old/New Trigger Split
Migrate all campaigns to NewTriggers = true and remove the legacy ResponderType branching code.