Skip to main content

CRM Integration — Technical Documentation

Also known as: CRM Push, CRM 2-Way Sync, CRM Deal Import, Integration Dispatch Last updated: March 2026


Feature Owners

RoleOwners
Product managersMartina Tuco, Kristijan Todorov
IT project managerMartin Davitkov
Lead developerMilena Atanasovska
StakeholdersKristijan Todorov, Katarina, Joe

Table of Contents

  1. Overview
  2. Architecture
  3. Repository Map
  4. Supported CRMs
  5. End-to-End Flows
  6. Key Entities & Data Model
  7. Services & Business Logic
  8. Queue & Background Processing
  9. Portal Configuration UI
  10. CRM Sync Trigger System
  11. Shared vs CRM-Specific Logic
  12. Configuration
  13. Integration Points
  14. 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 repositoriessopro-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:

SubsystemRepoPurpose
Event-Driven Pushsopro-sodastreamPush prospect data TO client CRMs when campaign events fire (email delivered, response classified, website visit)
2-Way Sync / Deal Importsopro-sodastream-corePull deals FROM client CRMs, match to Sopro prospects, store for ROI reporting
CRM Sync TriggersBoth reposFire CRM push events from newer sources (webchat, web intent, SDR, LinkedIn, gifting)

2. Architecture


3. Repository Map

sopro-sodastream (Legacy MVC5 API)

AreaKey FilesPurpose
Strategy InterfaceSoPro.API/Integrations/SoProIntegrations/ISoproIntegration.csEvent contract: OnDelivered, OnResponded, OnClicked, OnWebsiteVisit, OnCrmSyncTrigger
Base ClassSoPro.API/Integrations/SoProIntegrations/SoproIntegrationBase.csAbstract base with no-op defaults for all hooks
FactorySoPro.API/DispatcherHelpers/Helpers.csGetIntegrationInstance() — switch on Integration.Name string, manually news up implementations
DispatcherSoPro.API/Controllers/Dispatcher/EventDispatcherController.csHTTP POST endpoints receiving pipeline events, routes to CRM implementations
HubSpot PushSoPro.API/Integrations/Implementations/HubSpot/HubSpotIntegration.cs~506 LOC, creates companies/contacts/deals via HubSpot REST API
Salesforce PushSoPro.API/Integrations/Implementations/Salesforce/SalesforceIntegration.cs~631 LOC, uses Salesforce.Common/Salesforce.Force NuGet packages
Pipedrive PushSoPro.API/Integrations/Implementations/PipeDrive/PipeDriveIntegration.cs~309 LOC, token-based auth, custom field-key mapping
Zoho PushSoPro.API/Integrations/Implementations/Zoho/ZohoIntegration.cs~480 LOC, creates both Leads AND Contacts, domain-aware multi-region API
Zapier PushSoPro.API/Integrations/Implementations/Zapier/ZapierIntegration.cs~135 LOC, webhook-based, no direct CRM API
Worldpay PushSoPro.API/Integrations/Implementations/Worldpay/WorldpayIntegration.cs~208 LOC, largely commented out
HelpersServices/Integrations/Helpers/CRMHelpers.csEmail content reading from Azure Storage, company size normalization, industry mapping
EntitiesSoProEntities/Integration.cs, IntegrationCampaign.cs, IntegrationObject.cs, IntegrationError.csCore integration data model
EnumsSoProEntities/Enums/IntegrationEnum.csAll 9 CRM types with DB-matching integer values
API WrappersServices/Integrations/HubSpot/, Salesforce/, PipeDrive/, Zoho/Per-CRM REST client wrappers

sopro-sodastream-core (ASP.NET Core)

AreaKey FilesPurpose
Import ServicesCRMServices/HubspotImportService.cs, PipedriveImportService.cs, SalesforceImportService.cs, ZohoImportService.csPull deals from CRM APIs, match to Sopro prospects
Shared Deal ServiceCRMServices/CRMDealService.csPersists imported deals, prospects, companies, pipelines via Dapper
Queue ConsumerSoProQueueConsumer/Partials/TaskCRMTwoWaySyncHubspot.csDispatches to all 4 CRM import services (misleadingly named)
Batch ProcessingSoProQueueConsumer/Partials/TaskIntegrationProcessing.csProcesses backdate batch items from IntegrationScheduledProcessing
Trigger ServiceDapperSopro/Services/CRM/DapperCrmSyncTriggerService.csHTTP client that POSTs events to EventDispatcher endpoints
Portal ControllersPortal.Web/Controllers/HubSpotIntegrationController.cs, SalesforceIntegrationController.cs, PipedriveIntegrationController.cs, ZohoIntegrationController.csOAuth flows and settings wizards per CRM
Portal ListingPortal.Web/Controllers/IntegrationsController.csMain integrations page, IntegrationSettings deserializes per-CRM config
CRM EntitiesSoProEntities/CRMSync.cs, CRMSyncLog.cs, CRMDealImport.cs, CRMDealProspect.cs, CRMDealCompany.cs, CRMDealPipeline.cs, CRMDealPipelineStage.cs2-way sync data model
Batch EntitiesSoProEntities/IntegrationScheduledProcessing.cs, IntegrationScheduledProcessingBatch.csBackdate batch tracking
Enums (Dapper)DapperSopro/Enums/CRM/IntegrationEnum.cs4 CRMs only (HubSpot, Pipedrive, Salesforce, Zoho) — auto-incremented from 0, does NOT match legacy enum values
Trigger EnumDapperSopro/Enums/CRM/CRMSyncTriggerTypeEnum.cs27 trigger types
OAuth ConfigsCRMServices/Configs/HubSpotConfig.csHardcoded ClientId/Secret (HubSpot)

4. Supported CRMs

IntegrationEnum (canonical — sopro-sodastream)

SoProEntities/Enums/IntegrationEnum.cs:

CRMEnum ValuePushPull (2-Way)Portal OAuthStatus
HubSpot1YesYesHubSpotIntegrationControllerActive
Dynamics2Placeholder (no code)
Podio3Placeholder (no code)
Zoho4YesYesZohoIntegrationControllerActive
Sugar5Placeholder (no code)
Salesforce6YesYesSalesforceIntegrationControllerActive
Pipedrive7YesYesPipedriveIntegrationControllerActive
Worldpay72Partial (commented out)Legacy/Dead
Zapier78Yes (webhook)Active (push-only)

Dapper IntegrationEnum (sopro-sodastream-core)

DapperSopro/Enums/CRM/IntegrationEnum.csWARNING: 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 via Processor class
  • true — creates an IntegrationScheduledProcessing record 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 / EntityKey ColumnsPurpose
IntegrationId, Name, ClientId, SecretMaster CRM definition (OAuth credentials, display info)
IntegrationCampaignId, CampaignId, IntegrationId, JsonToken, JsonUserConfig, ProccessOnConsoleApplication, NewTriggersPer-campaign CRM configuration (tokens, field mappings, processing mode)
IntegrationObject[IntegrationId, ProspectEmailId] (composite PK), CrmProspectId, CrmCompanyId, CrmDealIdTracks CRM-side IDs for each synced prospect
IntegrationErrorIntegrationId, CampaignId, Stage, CountAggregated error tracking per integration/campaign
IntegrationObjectLogFK → IntegrationObjectAudit trail of integration object changes

Import-Side Entities (sopro-sodastream-core)

Table / EntityKey ColumnsPurpose
CRMSyncId, IntegrationId, CampaignId, ProspectId, CRMSyncTriggerTypeId, CrmProspectId, CrmCompanyId, CrmDealIdAudit record per CRM push event
CRMSyncLogCRMSyncId, LogDataDetailed log per CRM sync
CRMSyncTriggerTypeId, Description, PriorityLookup table for 27 trigger types
CRMDealImportId, IntegrationId, CampaignId, CrmDealId, CrmDealName, CrmDealAmount, CrmPipelineId, SoproProspectId, SoproMatchTypeImported deals from client CRMs
CRMDealImportLogSame shape (audit copy)Historical snapshot of each import run
CRMDealImportStatIntegrationId, CampaignId, StepNumber, RowsCount, DurationInSecondsPer-step import statistics
CRMDealProspectCrmDealId, CrmProspectId, SoproProspectId, Email, DetailsFromCRM contact → Sopro prospect match
CRMDealCompanyCrmDealId, CrmCompanyId, SoproCompanyId, CompanyDomain, DetailsFromCRM company → Sopro company match
CRMDealPipelineCrmPipelineId, CrmPipelineLabelCRM pipeline definitions
CRMDealPipelineStageCrmPipelineStageId, CrmPipelineStageLabel, CrmPipelineStageProbabilityPipeline stage definitions

Batch Processing Entities

Table / EntityKey ColumnsPurpose
IntegrationScheduledProcessingId, IntegrationId, ProspectId, CampaignId, Status, Priority, Action, CrmSyncTriggerTypeOne queue item per prospect to process
IntegrationScheduledProcessingBatchId, CampaignId, IntegrationId, TotalIntegrations, ProcessedIntegrations, FailedIntegrationsBatch 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:

MethodWhen CalledTypical Action
OnAddedCampaign createdNo-op for most CRMs
OnSentEmail sentCreate company (no contacts)
OnSentBatchBatch email sendCreate companies + contacts
OnDeliveredEmail deliveredGet-or-create Company → Contact → save IntegrationObject
OnOpenedEmail openedNo-op for most CRMs
OnClickedLink clickedNo-op for most CRMs
OnRespondedResponse classifiedSame as delivered + Create Deal (stage based on response type)
OnWebsiteVisitTracked website visitNo-op or create record depending on CRM
OnCrmSyncTriggerNew trigger systemMap 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:

ServiceFileMethods
IHubspotImportServiceCRMServices/HubspotImportService.csCRM_ImportDeals(), RefreshToken()
ISalesforceImportServiceCRMServices/SalesforceImportService.csCRM_ImportDeals(), RefreshToken()
IPipedriveImportServiceCRMServices/PipedriveImportService.csCRM_ImportDeals(), RefreshToken()
IZohoImportServiceCRMServices/ZohoImportService.csCRM_ImportDeals(), RefreshToken()

All four follow the same internal pattern:

  1. Refresh OAuth token
  2. Fetch all deals and pipelines from CRM API
  3. Upsert to CRMDealImport table
  4. Run stored procedures to match CRM contacts/companies to Sopro data
  5. 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)IntQueue Name PatternPurpose
IntegrationHub9integrationhubHubSpot scheduled push processing
IntegrationSalesForce10integrationsalesforceSalesforce scheduled push processing
IntegrationPipedrive11integrationpipedrivePipedrive scheduled push processing
IntegrationZoho12integrationzohoZoho scheduled push processing
IntegrationZapier27integrationzapierZapier scheduled push processing
CrmTwoWaySyncHubspot41crm-2-way-sync-hubspot2-way sync for ALL 4 CRMs (not just HubSpot)

Queue Consumer Tasks

Task FileQueueBehavior
TaskCRMTwoWaySyncHubspot.csCrmTwoWaySyncHubspot (41)Checks for HubSpot (ID=1), Pipedrive (ID=7), Salesforce (ID=32), Zoho (ID=35) integrations on the campaign, calls each CRM_ImportDeals()
TaskIntegrationProcessing.csPer-CRM queues (9–12, 27)Processes individual IntegrationScheduledProcessing items from backdate batches

Batch Processing Model

For backdate operations (pushing historical data to CRMs):

  1. IntegrationScheduledProcessingBatch is created with total count
  2. One IntegrationScheduledProcessing record per prospect (status: Pending)
  3. Items are enqueued to CRM-specific queue
  4. Queue consumer processes each item, updates status to Processed or Failed
  5. Batch tracks ProcessedIntegrations and FailedIntegrations counts

9. Portal Configuration UI

OAuth Flow

Each CRM has a dedicated Portal controller (Portal.Web/Controllers/) handling:

StepController ActionDescription
1. InitiateAuthorize(campaignId) / GoToZoho(campaignId, domain)Generates OAuth URL, stores campaignId in session
2. CallbackToken(code) / Token(code, state)Exchanges auth code for tokens, fetches CRM pipelines/fields
3. WizardReturns _AuthWizard.cshtml partialRenders field mapping, pipeline selection, stage mapping UI
4. SubmitSubmit(userSettings, token, ...)Saves to IntegrationCampaign.JsonToken + IntegrationCampaign.JsonUserConfig
5. EditEditUserSettings(id)Loads existing config, refreshes CRM metadata, shows edit wizard
6. DeleteDelete(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:

SettingTypePurpose
ImportOnSendboolSync on email delivery
addOpportunitiesboolCreate deals/opportunities
stage{TriggerType}stringDeal stage ID when trigger fires
stage{TriggerType}NamestringDeal stage display name
leadStatus{TriggerType}stringLead status per trigger type (18 variants each)
sourceKey / sourceValuestringSource attribution for created CRM records
mappingDictionary<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:

CategoryTriggerValue
Legacy ResponsePositiveResponse2
Referral3
DeferredInterestKeepinTouch4
PoliteDecline5
EmailEmailDelivered100
Web IntentWebsiteVisit110
WebIntentLead130
MultipleVisits180
ReturningInterest181
GoalUrl182
WebchatWebchatLead120
WebchatSession190
InboundWebchat201
LinkedInPositiveResponseLinkedIn170
ReferralLinkedIn171
DeferredInterestLinkedIn172
PoliteDeclineLinkedIn173
ActiveDialogueLinkedIn174
SDRCalledPitchFollowUpBooked200
InboundWebform202
InboundCallPositive203
OtherGiftRedemptionLead140
ExitIntentFormSubmissionLead150
ManualEventLeadOrSDRLead160
PhoneNumber210
FallbackUnknown255

Trigger Sources

ServiceRepoTriggers Fired
SoproChatControllersopro-sodastream-coreWebchatSession, InboundWebchat
SoproChatActivityServicesopro-sodastreamInboundWebchat, WebchatSession
SalesDevelopmentServicesopro-sodastreamInboundWebform, InboundCallPositive, InboundWebchat
ProspectIntentTrackingServicesopro-sodastreamReturningInterest, MultipleVisits, GoalUrl
WebsiteWidgetServicesopro-sodastreamWebsiteVisit
ResponderMessageImapServicesopro-sodastreamResponse-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 from ResponderType enum values
  • true (new): Events use CRMSyncTriggerTypeEnum with 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

ComponentLocationNotes
ISoproIntegration + SoproIntegrationBasesopro-sodastreamStrategy contract with no-op defaults
IntegrationObject trackingsopro-sodastreamAll CRMs store CrmProspectId, CrmCompanyId, CrmDealId
CRMSync + CRMSyncLog auditsopro-sodastream-coreShared audit entities
CRMDealServicesopro-sodastream-coreAll import services use this for persistence
Stored procedures (Steps 2, 5, 7)DatabaseCRM-agnostic matching logic
IntegrationScheduledProcessing batchsopro-sodastream-coreSame batch framework for all CRMs
UserSettings base fieldsBoth reposImportOnSend, addOpportunities, trigger-stage/status mappings

CRM-Specific Differences

AspectHubSpotSalesforcePipedriveZohoZapier
AuthOAuth2 (hardcoded ClientId/Secret in HubSpotConfig.cs)OAuth2 with sandbox/production toggleAPI token (not OAuth)OAuth2 with multi-region domainNone (webhook URLs)
CRM objects createdCompanies, Contacts, Deals, Engagements, Timeline ActivitiesAccounts, Contacts, Leads, Opportunities, TasksOrganizations, Persons, DealsAccounts, Contacts, Leads, DealsWebhook payloads only
Contact modelContacts onlyContacts AND LeadsPersonsContacts AND LeadsN/A
Pipeline handlingPer-trigger pipeline + stage mappingStages only, no per-trigger pipelineStages onlyStages onlyN/A
Owner mappingEmail → HubSpot Owner ID lookupSalesforce User lookupOwner lookupZoho User lookupN/A
Field mapping UITable with CRM field dropdownsSameSame (with custom field-key mapping)SameN/A
Token refreshHubSpotWrapper.RefreshToken()AuthenticationClient.TokenRefreshAsync()Reuses HubSpotWrapper.RefreshToken()ZohoWrapper.RefreshToken()N/A
Pull/ImportFull deal import with pipeline stagesOpportunity importDeal importDeal importNone
Unique behaviorsaddTimelineActivity, lifecycleStageSandbox flag, recordType, requiredFieldsCustom field-key systemuseCompanyMapping, dealClosingDateDays, multi-domainWebhook subscriptions list in JsonToken

Duplicated Code

CodeCopies FoundLocations
CRMHelpers2sopro-sodastream/Services/Integrations/Helpers/CRMHelpers.cs (~313 LOC), sopro-sodastream-core/Portal.Services/DTOs/Integrations/Helpers/CRMHelpers.cs (~310 LOC)
CRMSyncTriggerTypeEnum2+DapperSopro/Enums/CRM/, Portal.Services/Enums/
IntegrationEnum3sopro-sodastream (9 values), sopro-sodastream-core Dapper (4 values, mismatched IDs), sopro-sodastream-core SoProEntities (copy of legacy)
Token refresh pattern4Each *ImportService copy-pastes the same RefreshTokenResult logic
UserSettings class3+Per CRM, per project (Portal.Services, DapperSopro, CRMServices)

12. Configuration

IntegrationCampaign Key Fields

FieldTypePurpose
JsonTokenstring (JSON)Serialized OAuth tokens — refreshed on each use
JsonUserConfigstring (JSON)Serialized user settings (field mappings, stages, pipelines)
RefreshTokenstringStored separately for easy access
ProccessOnConsoleApplicationboolfalse = inline push, true = queued batch push
NewTriggersboolfalse = legacy ResponderType mapping, true = CRMSyncTriggerTypeEnum
EnableBackDateFromClientboolAllow backdating historical data to CRM

OAuth Credentials

CRMStorageNotes
HubSpotHardcoded in CRMServices/Configs/HubSpotConfig.csMultiple commented-out values for different environments
SalesforceIntegration.ClientId + Integration.Secret (DB)Has sandbox toggle per campaign
ZohoIntegration.ClientId + Integration.Secret (DB)Domain-specific
PipedriveAPI token stored in IntegrationCampaign.JsonTokenNot OAuth2, simpler token model

Queue Processing Config

SettingSourcePurpose
APIWebsiteGeneralSettings (DB)URL for DapperCrmSyncTriggerService to POST to EventDispatcher
Queue namesConsoleTypeEnumAzure queue routing for each CRM

13. Integration Points

Cross-Repo Communication

FromToMechanismDetails
sopro-sodastream-core trigger sourcessopro-sodastream EventDispatcherHTTP POSTDapperCrmSyncTriggerService/EventDispatcher/OnCrmSyncTrigger/
sopro-sodastream-core trigger sourcessopro-sodastream EventDispatcherHTTP POSTDapperCrmSyncTriggerService/EventDispatcher/OnWebhookCrmSyncTrigger/
sopro-sodastream EventDispatcherAzure queuesAzure QueueEnqueues IntegrationScheduledProcessing to per-CRM queues (types 9–12, 27)
Azure queue (type 41)sopro-sodastream-core TaskCRMTwoWaySyncHubspotAzure QueueCampaign ID message triggers 2-way sync
sopro-sodastream push implementationsExternal CRM APIsRESTHubSpot, Salesforce, Pipedrive, Zoho, Zapier webhook URLs
sopro-sodastream-core import servicesExternal CRM APIsRESTPull deals, pipelines, contacts, companies
sopro-sodastream-core PortalExternal CRM OAuth serversOAuth2Token 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 reporting
  • CRMDealImport, CRMDealProspect, CRMDealCompany, CRMDealPipeline, CRMDealPipelineStage — written by import services
  • IntegrationScheduledProcessing, IntegrationScheduledProcessingBatch — written by EventDispatcher, processed by queue consumer

14. Known Pain Points & Future Improvements

14.1 Architectural Issues

IssueImpactDetails
Massive code duplicationAdding a new CRM requires copy-pasting thousands of LOCCRMHelpers 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 DIUntestable, fragileHelpers.GetIntegrationInstance() uses new with ~12 constructor parameters, string-based routing via Integration.Name
Split across two reposFeature changes touch both reposPush logic in sopro-sodastream (MVC5), pull/import in sopro-sodastream-core (ASP.NET Core), Portal UI in sopro-sodastream-core
4000+ LOC integration classesHard to maintain, test, or extendEach CRM pushes all event handling into a single massive class
Inconsistent enum definitionsRisk of ID mismatchesDapper IntegrationEnum auto-increments from 0, legacy enum uses DB-matching IDs (1, 2, 4, 6, 7, 72, 78)
No shared import interfaceEach new CRM needs bespoke dispatch codeTaskCRMTwoWaySyncHubspot hardcodes the integration IDs and individually calls each import service
Old vs New trigger splitPermanent branching complexityNewTriggers flag means every CRM must handle both ResponderType and CRMSyncTriggerTypeEnum paths
Heavy use of dynamicRuntime failures, no compile-time safetyHubSpot, Pipedrive, and Zoho extensively deserialize API responses as dynamic

14.2 Specific Code Smells

SmellLocationDetails
Hardcoded OAuth secretsCRMServices/Configs/HubSpotConfig.csClientId and Secret as string constants with commented-out staging values
Client-specific hacksEventDispatcher, Zoho integrationcampaign.ClientId == 2806 checks, hardcoded email-to-owner mappings
Misleading namingTaskCRMTwoWaySyncHubspotProcesses ALL 4 CRMs, not just HubSpot
Commented-out codeWorldpayIntegration.OnDeliveredEntire method body commented out, class still active
Pipedrive reuses HubSpot token refreshPipedriveImportService.RefreshToken()Calls HubSpotWrapper.RefreshToken() for Pipedrive tokens

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 CRMHelpers in a shared library
  • Single IntegrationEnum with 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 UserSettings classes

4. Consolidate to One Repo

Move push logic from sopro-sodastream to sopro-sodastream-core to eliminate the cross-repo HTTP dependency (DapperCrmSyncTriggerServiceEventDispatcher).

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.