Campaigns Service
### getAddedTrackingNumbers(req, res, next)// Success - 200
{
**Purpose**: Retrieve list of CallRail tracking numbers already added to campaigns success: true,
message: "SUCCESS",
**Parameters**: data: [
- `req.auth.account_id` (ObjectId) - Account context {
- `req.auth.uid` (ObjectId) - Campaign owner _id: ObjectId,
- `req.query.type` (String, optional) - Filter type campaign_name: "Summer Lead Gen Campaign",
status: false,
**Returns**: selected_user: [...],
```javascript updated_at: Date
{ }
success: true, ]
message: 'SUCCESS',}
data: [```
{
tracking_number: String,**Campaign Disconnection Notification**:
campaign_id: ObjectId,
campaign_name: String```mermaid
}sequenceDiagram
] participant Client
} participant API
``` participant MongoDB
participant FCM
**Business Logic Flow**: participant Email
participant Socket
1. **Build Query**
```javascript Client->>API: PUT /campaigns/:id (status: false)
let campaignData = { API->>MongoDB: Update campaign status
account_id: accountId.toString(), MongoDB-->>API: Campaign updated
owner: owner.toString(),
is_deleted: false alt Campaign disabled or deleted
}; API->>FCM: Push notification
``` API->>Email: Email notification
Note right of Email: Subject: Inbound Campaign Disconnected
2. **Get Tracking Numbers** end
```javascript
let trackingNumbers = await campaignDataModel.getAllAddedTrackingNumbers({ API->>Socket: Emit campaigncountupdated
campaignData: campaignData, API-->>Client: 200 Success
type: type,```
});
```**MongoDB Operations**:
- Model filters campaigns with tracking_number field
- Returns array of tracking numbers with campaign info| Operation | Collection | Query | Purpose |
| ------------ | --------------- | ----------- | ------------------- |
3. **Return List**| `update` | `campaign-data` | `{_id}` | Update campaign |
- Used for validation when creating new CallRail campaigns| `getFilters` | `campaign-data` | Aggregation | Get updated filters |
- Prevents duplicate tracking number entry
**Automatic Notifications**:
**Key Business Rules**:When campaign is disabled (`status: false`) or deleted (`is_deleted: true`):
- **Active Campaigns Only**: Excludes soft-deleted campaigns
- **Owner Scoping**: Only returns owner's tracking numbers#### 1. Push Notification (FCM)
- **Validation Support**: Used by UI to show already-used numbers
```javascript
**Example Usage**:{
```javascript title: "Inbound Campaign Disconnected.",
const numbers = await getAddedTrackingNumbers({ body: "Your {campaign_name} integration/webhook has disconnected from your Inbound app. We recommend reconnecting the integration/webhook to avoid any disruption in service.",
auth: { account_id, uid }, data: {
query: {} campaign_id: String
}); },
// Returns list of tracking numbers to validate against module: "inbound",
``` type: "campaign_disconnected"
}
**Side Effects**: None (read-only)```
---#### 2. Email Notification
### updateCampaign(req, res, next)```javascript
{
**Purpose**: Update existing campaign configuration with disconnection notifications subject: "Inbound Campaign Disconnected.",
content: [
**Parameters**: {
- `req.auth.account_id` (ObjectId) - Account context value: "Your {campaign_name} integration/webhook has disconnected..."
- `req.auth.uid` (ObjectId) - Owner ID }
- `req.params.campaignid` (ObjectId) - Campaign to update ],
- `req.body.campaigndata` (Object) - Fields to update origin: "campaign_disconnected"
}
**Returns**:```
```javascript
{**Socket.IO Events**:
success: true,
message: 'SUCCESS',```javascript
data: [Campaign]// Emitted to campaign owner with updated filters
}utilities.socketEmit('campaigncountupdated', [owner.toString()], filtersData);
Business Logic Flow:Business Rules:
-
Extract Parameters1. Selected Users: IDs converted to ObjectIds before saving
const accountId = req.auth.account_id;3. **Notification Trigger**: Only sends notifications when `status` changes to `false` or `is_deleted` becomes `true`
const owner = req.auth.uid;4. **Error Logging**: Notification failures logged but don't block update operation
let campaignId = req.params.campaignid;
let campaignData = req.body.campaigndata;---
Data Modelsโ
-
Convert Selected Users to ObjectId
if (campaignData.selected_user) {
let suser = [];```javascript
for (let user of campaignData.selected_user) {{
user.id = new mongoose.Types.ObjectId(user.id); _id: ObjectId,
suser.push(user); campaign_name: String, // Campaign display name
} integration: String, // Integration type (see list above)
campaignData.selected_user = suser; account_id: ObjectId, // DashClicks account
} owner: ObjectId, // Campaign creator/owner
// External IDs
-
Handle Currency Updates page_id: String, // Facebook page ID, form ID, etc.
if (campaignData.currency) { page_url: String, // Landing page URL (trailing slash removed)
campaignData.currency = campaignData?.currency; tracking_number: String, // Phone tracking number (CallRail/CTM)
}
``` // InstaForms Integration
inhouse_form_id: ObjectId, // Generated form ID
-
Update Campaign inhouse_form_secret_id: String, // UUID for form access
let campaignupdate = await campaignDataModel.update( // Representative Assignment
{ _id: campaignId }, selected_user: [
campaignData {
); id: ObjectId, // User ID
``` name: String, // User display name
- Returns updated campaign with populated fields custom_number: String // Custom phone for SMS (optional)
} -
Refresh Filters and Emit Socket ],
let filtersData = await campaignDataModel.getFilters({ // Notification Configuration
accountId: accountId.toString(), notification_type: String, // "only_send_email" | "only_send_sms" | "send_email_sms"
owner: owner.toString(), email_details: {
req: req, subject: String,
}); message: String, // Supports template variables
from: String,
utilities.socketEmit('campaigncountupdated', [owner.toString()], filtersData); attachments: Array,
``` reply_to: {
email: String, -
Send Disconnection Notifications name: String
if (campaignData?.status == false || campaignData?.is_deleted == true) { },
if (campaignupdate?.[0]?.campaign_name) { sms_details: {
// FCM Push Notification message: String, // Supports template variables
let notification = { from: String // Twilio phone number
title: `Inbound Campaign Disconnected.`, },
body: `Your ${campaignupdate[0].campaign_name} integration/webhook has disconnected from your Inbound app. We recommend reconnecting the integration/webhook to avoid any disruption in service.`,
data: { // CRM Integration
campaign_id: campaignupdate[0].id ? campaignupdate[0].id.toString() : null, add_to_crm: Boolean, // Auto-create contacts (default: true)
}, add_person_or_business: String, // "person" | "business"
module: 'inbound',
type: 'campaign_disconnected', // Deal Automation
}; create_deal: Boolean, // Auto-create deals (default: false)
pipeline_id: ObjectId, // CRM pipeline for deals
sendFCMNotification({ stage_id: ObjectId, // Initial pipeline stage
req: req, deal_value: Number, // Default deal value
notification: notification, currency: String, // Deal currency (e.g., "USD", "EUR")
module: 'inbound',
type: 'campaign_disconnected', // Status
}); status: Boolean, // Active/inactive (default: true)
is_deleted: Boolean, // Soft delete flag (default: false)
// Email Notification
let mailBody = { // Metadata
subject: `Inbound Campaign Disconnected.`, created_at: Date,
content: [ updated_at: Date
{}
value: `Your ${campaignupdate[0].campaign_name} integration/webhook has disconnected from your Inbound app. We recommend reconnecting the integration/webhook to avoid any disruption in service.`,```
},
],---
origin: 'campaign_disconnected',
};## Integration Points
sendMailNotification({### Internal Services
req: req,
mailBody: mailBody,- **CRM Module**: Contact and deal creation
module: 'inbound',- **Forms Module**: InstaForms template and creation
type: 'campaign_disconnected',- **Notification Service**: FCM push, email, SMS
});- **Socket.IO**: Real-time campaign updates
}
}### External Services-
Triggers when campaign disabled (status: false)- Facebook Graph API: Lead Ads integration
-
Triggers when campaign deleted (is_deleted: true)- Twilio: SMS notifications
-
Sends both push and email notifications- SendGrid: Email notifications
-
Errors logged but don't fail update
-
Utilitiesโ
- Return Updated Campaign
AppendCopyToName: Ensures unique form names
Key Business Rules:- socketEmit: Real-time updates to frontend
-
Disconnection Alerts: Automatic notifications when campaigns disabled/deleted-
checkAccountStatus: Validates account is active -
Multi-Channel Notifications: Both FCM push and email sent
-
Error Tolerance: Notification failures logged but update succeeds---
-
Socket Updates: Real-time filter updates to connected clients
-
Currency Tracking: Supports currency field updates## Error Handling
Notification Trigger Conditions:### Common Errors
-
status: false- Campaign manually disabled -
is_deleted: true- Campaign soft deleted| Error | Status | Message | Cause |
| ------------------------- | ------ | -------------------------------- | ----------------------------------------- |
Example Usage:| Duplicate tracking number | 409 | "Tracking number already added!" | Tracking number exists in active campaign |
// Update campaign settings| Invalid campaign | 404 | Campaign not found | Campaign doesn't exist or deleted |
await updateCampaign({
auth: { account_id, uid },---
params: { campaignid: 'campaign_id' },
body: {## Testing Recommendations
campaigndata: {
notification_type: 'send_email_sms',### Unit Tests
email_details: {
subject: 'New Lead',1. **saveCampaign**:
message: 'You have a new lead!',
from: 'leads@example.com' - Test InstaForms auto-creation
} - Test tracking number uniqueness validation
} - Test page URL normalization
} - Test selected_user ObjectId conversion
}); - Test socket emission
// Disable campaign (triggers notifications)2. **updateCampaign**:
await updateCampaign({
auth: { account_id, uid }, - Test status change notification trigger
params: { campaignid: 'campaign_id' }, - Test currency field support
body: { - Test socket emission with filters
campaigndata: {
status: false3. **getCampaignsInfo**:
}
} - Test role-based filtering
}); - Test pagination (page-based and cursor-based)
// Result: Campaign disabled, FCM and email notifications sent - Test search functionality
- getAddedTrackingNumbers:
Side Effects: - Test type filtering
-
Updates Campaign: Modifies campaign document - Test account isolation
-
Socket Emission: Real-time filter updates
-
Sends Notifications: FCM push and email on disconnection### Integration Tests
-
Logs Errors: Notification failures logged (non-blocking)
-
Full campaign lifecycle (create โ update โ disable)
---- Notification delivery on campaign disconnect
- Socket event propagation
๐ Integration Points- InstaForms template integrationโ
Internal Services---โ
-
Forms Service (link removed - file does not exist):## Performance Considerations
-
Creates in-house forms from templates
-
Generates form_secret_id for public access- Pagination: Use cursor-based for large datasets
-
Links forms to campaigns via inhouse_form_id- Indexing: Ensure indexes on
account_id,tracking_number,integration,is_deleted
-
-
Socket Optimization: Only emit to campaign owner, not all users
-
Notifications Service (link removed - file does not exist):- Aggregation: Filter counts cached where possible
-
sendFCMNotification()- Push notifications for campaign events -
sendMailNotification()- Email alerts for disconnections---
-
-
Socket Service (link removed - file does not exist):## Security Considerations
-
utilities.socketEmit()- Real-time campaign count updates -
Event: 'campaigncountupdated' with filter data1. Multi-Tenant Isolation: All queries filtered by
account_id
-
- Role-Based Access: Non-admins only see assigned campaigns
Utilities3. Tracking Number Validation: Prevents duplicate assignmentsโ
- Soft Delete: Preserves historical data
-
AppendCopyToName (link removed - file does not exist):5. Notification Rate Limiting: Consider rate limiting for notification failures
-
Generates unique names with " (Copy)" suffix
-
Prevents duplicate campaign/form names---
-
Async promise-based interface
-
Last Updated: October 2025
-
UUID Generator (link removed - file does not exist):Status: Production Ready
-
Generates form_secret_id for public form accessComplexity: MEDIUM
-
v4 UUIDs for cryptographic randomness
-
External Dependenciesโ
- Mongoose Models:
- campaignDataModel - Campaign CRUD operations
- formsModel - Form creation and management
- templatesModel - Form template lookup
๐งช Edge Cases & Special Handlingโ
CallRail Tracking Number Edge Casesโ
Duplicate Detection:
// Only checks active campaigns
let condition = {
tracking_number: '4155551234',
is_deleted: false,
};
// Soft-deleted campaigns excluded from uniqueness check
Null/Undefined Tracking Numbers:
- Validation skipped if tracking_number not provided
- Only CallRail integration requires tracking numbers
URL Normalization Edge Casesโ
Multiple Trailing Slashes:
// Input: "https://example.com///"
// After trim: "https://example.com///"
// After slice: "https://example.com//"
// Issue: Only removes last slash
Query Parameters:
// Input: "https://example.com/page?param=value/"
// Output: "https://example.com/page?param=value"
// Correct: Handles query strings properly
Fragment Identifiers:
// Input: "https://example.com/page#section/"
// Output: "https://example.com/page#section"
// Correct: Handles hash fragments
Form Generation Edge Casesโ
Template Not Found:
let getTemplate = await templatesModel.findOne({ form_name: 'InstaSites' });
if (!getTemplate?._id) {
// Form fields not set on campaign
// campaignData.inhouse_form_id remains undefined
// Campaign still created but without form
}
Form Name Conflicts:
- AppendCopyToName adds " (Copy)", " (Copy 2)", etc.
- Searches existing campaigns collection for name conflicts
- Prevents duplicate form names within account
Missing Template Categories:
delete getTemplate.categories; // Remove before copying
delete getTemplate._id; // Remove template _id
// Prevents template data pollution in new forms
Representative Assignment Edge Casesโ
Empty Selected Users Array:
campaignData.selected_user = [];
// Valid: Campaign has no assigned representatives
// Leads will have user_id: null
Invalid User IDs:
// Model validation catches invalid ObjectId references
// Error thrown during save: "inhouse_form_id validation failed"
Custom Number Handling:
{
id: 'user_id',
custom_number: null // Valid: Uses default SMS number
}
{
id: 'user_id',
custom_number: '+14155551234' // Override SMS number for this user
}
Notification Edge Casesโ
Notification Failures:
try {
sendFCMNotification(...);
sendMailNotification(...);
} catch (error) {
logger.error({ error, message: 'Notification failed' });
// Error logged but update succeeds
// User still updated, notifications just failed
}
Multiple Status Changes:
// Update 1: status: false (sends notification)
// Update 2: status: true (no notification)
// Update 3: status: false (sends notification again)
// Each status change to false triggers new notifications
Deleted Campaign Notifications:
// is_deleted: true also triggers disconnection notification
// Prevents confusion when campaigns disappear from UI
โ ๏ธ Important Notesโ
-
Tracking Number Uniqueness: CallRail tracking numbers must be unique across all active campaigns in account. Soft-deleted campaigns excluded from check. Always validate before creating CallRail campaigns.
-
URL Normalization: page_url automatically normalized by removing trailing slash. Only removes last slash - doesn't handle multiple slashes. URLs stored consistently for matching and display.
-
Form Generation: InstaSite/InstaReport campaigns automatically create forms from templates. Form name generated with AppendCopyToName to avoid conflicts. form_secret_id is public UUID, not MongoDB _id.
-
Representative Assignment: selected_user array must have ObjectId for id field. custom_number optional for SMS override. Empty array valid (no representatives assigned).
-
Disconnection Notifications: Sent when status set to false OR is_deleted set to true. Both FCM push and email notifications sent. Notification failures logged but don't block update.
-
Socket Updates: 'campaigncountupdated' event emitted to owner's socket on create/update. Filter data included in event. Enables real-time dashboard updates without polling.
-
Role-Based Access: Non-admin users only see campaigns in selected_user array. Admin users see all account campaigns. Always apply role filter in queries.
-
Currency Support: Optional currency field for multi-currency tracking. Used in deal creation to set deal currency. Defaults to account currency if not specified.
-
Soft Delete Behavior: is_deleted: false filter applied to all queries. Soft-deleted campaigns excluded from tracking number uniqueness. Preserves historical data while hiding from UI.
-
Form Template Lookup: Templates identified by form_name ('InstaSites' or 'InstaReport'). Template categories removed before copying. Template _id removed to create new document.
๐ Related Documentationโ
- Leads Service - Lead capture and management
- Webhooks Service - Webhook handling for integrations
- Reports Service - Campaign analytics and reporting
- Forms Service (link removed - file does not exist) - Form creation and management
- Notifications Service (link removed - file does not exist) - Push and email notifications
- Socket Utility (link removed - file does not exist) - Real-time updates