Onboardings
Overview
The Onboardings service manages the complete onboarding workflow for managed services, including Typeform integration, status tracking, response management, and email notifications for form submissions.
Source Files:
- Service:
internal/api/v1/projects/services/onboardings.service.js - Controller:
internal/api/v1/projects/controllers/onboardings.controller.js
Key Capabilities:
- List onboardings with status-based filtering
- Retrieve single onboarding with response data
- Update typeform responses with change requests
- Resend onboarding forms via email
- Track onboarding workflow states (sent → received → approved)
- Client dashboard visibility control
- Date field scoping for sensitive information
Collections Used
_store.orders
Operations: Read, Update
Usage: Primary onboarding data source with order context
onboarding.typeform.response
Operations: Read, Update
Usage: Stores typeform submission responses
onboarding.typeform.requests
Operations: Read
Usage: Tracks typeform send requests
_store.subscriptions
Operations: Read (lookup)
Usage: Active subscription validation, typeform link storage
crm.contacts
Operations: Read (lookup)
Usage: Buyer person and business contact information
_users
Operations: Read (lookup)
Usage: Account owner information for form recipients
projects.tasks
Operations: Read (lookup)
Usage: Latest onboarding task for event type tracking
notification-queue
Operations: Create
Usage: Email notification queueing for form resends
Business Logic & Functions
Service Layer
getOnboardings({ accountInfo, skip, limit, sortby, sort })
Purpose: Retrieves paginated onboarding list with complex status filtering and enrichment.
Parameters:
accountInfo(Object) - Containsaccount_idanddashboardPreferencesskip(Number) - Pagination offsetlimit(Number) - Page sizesortby(String) - Sort field ('status', 'sent_to', or default 'updated_at')sort(String) - Sort direction ('asc' or 'desc')
Returns: { data: [], total: Number }
Workflow State Machine:
sent → received → approved
Client Dashboard Filtering:
if (dashboardPreferences?.allow_client_dashboard || dashboardPreferences?.sso) {
// Only show 'approved' OR 'sent' with metadata.send_onboarding_to === 'client'
options.$and = [
{
$or: [
{
$and: [
{ 'onboarding.status.status': 'sent' },
{ 'onboarding.status.status': { $nin: ['received', 'approved'] } },
],
},
{ 'onboarding.status.status': 'approved' },
],
},
];
}
Latest Event Type Logic:
// 1. Extract latest onboarding status
latest_onboarding_status = $arrayElemAt(
$filter(onboarding.status, where updated_at !== null),
-1 // Last element
);
// 2. Find latest task matching ^onboarding regex
latest_task = projects.tasks
.where(order_id === orderId && type.startsWith('onboarding'))
.sort(updated_at: -1)
.limit(1);
// 3. Override with task type if exists
latest_event_type = latest_task.type ?? latest_onboarding_status.status;
Sent Event Metadata Filtering:
// If client dashboard, filter 'sent' events by metadata
if (isDashboardEnabled) {
$match: {
$expr: {
$or: [
{ $ne: ['$latest_event_type', 'sent'] }, // Non-sent always shown
{
$and: [
{ $eq: ['$latest_event_type', 'sent'] },
{ $eq: ['$subscriptions.metadata.send_onboarding_to', 'client'] },
],
},
];
}
}
}
Target Recipient Determination:
targetUserOrContact = {
$cond: [
{ $eq: ['$subscriptions.metadata.send_onboarding_to', 'me'] },
owner_user[0], // Send to agency owner
buyer_contact[0], // Send to client contact
],
};
Date Scoping:
onboardingDatesDisabled = !dashboardPreferences?.scopes?.includes('activity.onboarding_dates');
// If disabled, remove createdAt/updatedAt from responses
if (onboardingDatesDisabled) {
- Remove from onboarding_response
- Set completed_on to $$REMOVE
- Set sent_on to $$REMOVE
}
Typeform Link Generation:
typeform_link = {
$cond: {
if: {
$and: [
{ $ne: [subscription.typeform.link, null] },
{ $ne: [subscription.typeform.link, ''] },
{ $ne: [requests._id, null] },
],
},
then: subscription.typeform.link + '#req_id=' + requests._id,
else: null,
},
};
Response Structure:
{
data: [{
onboarding_response: [], // Array of form responses (dates removed if scoped)
latest_event_type: String, // sent|received|approved|onboarding_*
buyer_business: ObjectId,
buyer_account: ObjectId,
seller_account: ObjectId,
subscription: ObjectId,
targetName: String, // Recipient name
targetEmail: String, // Recipient email
targetImage: String,
product_name: String, // "Product Name (Tier)"
metadata: Object,
completed_on: Date, // Removed if dates disabled
sent_on: Date, // Removed if dates disabled
updated_at: Date,
typeform_link: String, // Full link with req_id param
requests: []
}],
total: Number
}
getOrderOnboarding({ orderId, accountId, dashboardPreferences })
Purpose: Retrieves onboarding response for a specific order.
Parameters:
orderId(ObjectId) - Order IDaccountId(ObjectId) - Seller account IDdashboardPreferences(Object, optional) - Client dashboard settings
Returns: Array of onboarding response documents
Access Control:
const orderOptions = {
'orderInfo.seller_account': accountId,
};
if (dashboardPreferences?.allow_client_dashboard || dashboardPreferences?.sso) {
orderOptions['orderInfo.buyer_account'] = {
$in: dashboardPreferences.sub_account_ids,
};
orderOptions['orderInfo.seller_account'] = dashboardPreferences.parent_account;
}
Date Scoping:
if (OnboardingResponse.length > 0 && onboardingDatesDisabled) {
delete OnboardingResponse[0].createdAt;
}
updateOrderOnboardingService(responseId, payload, account_id, user_id)
Purpose: Updates typeform response answers with validation and change tracking.
Parameters:
responseId(ObjectId) - Response document IDpayload(Array) - Array of field updates with values and change requestsaccount_id(ObjectId) - Account performing updateuser_id(ObjectId) - User performing update
Returns: Updated response document
Typeform Field Type Mapping:
const TYPEFORM_VALUE_KEYS_MAP = {
short_text: 'text',
long_text: 'text',
email: 'email',
phone_number: 'phone_number',
website: 'url',
number: 'number',
yes_no: 'boolean',
legal: 'boolean',
file_upload: 'file_url',
opinion_scale: 'number',
rating: 'number',
date: 'date',
dropdown: 'choice',
picture_choice: 'choice',
ranking: 'choices',
matrix: 'choices',
nps: 'number',
multiple_choice: undefined, // Special handling
statement: null, // No response
welcome_screen: null,
thank_you_screen: null,
question_group: null,
};
Update Logic:
-
Fetch Existing Response and build field definition map
-
Process Each Payload Item:
payload.forEach((item, index) => {
const field = fieldsMap.get(item.id);
const fieldType = field.type;
const valueKey = TYPEFORM_VALUE_KEYS_MAP[fieldType];
if (fieldType !== 'multiple_choice') {
updateFields[`form_response.answers.$[answer${index}].${valueKey}`] = item.value;
} else {
// Special multiple choice handling
if (field.allow_multiple_selections) {
updateFields[`form_response.answers.$[answer${index}].choices`] = {
ids: [...],
labels: [...],
refs: [...]
};
} else {
updateFields[`form_response.answers.$[answer${index}].choice`] = {
id, ref, label
};
}
}
if (item.changes_requested) {
changesRequested[item.id] = item.changes_requested[item.id];
}
if (item.resolved) {
isResolved = true;
}
}); -
Update with Array Filters:
const arrayFilters = payload.map((item, index) => ({
[`answer${index}.field.id`]: item.id,
}));
await onboardingTypeformResponse.updateOne(
{ _id: responseId },
{ $set: updateFields },
{ arrayFilters },
); -
Create Activity Log:
await createActivity({
refId: existingDocument.order,
accountId: account_id,
parentAccount: orderInfo.seller_account,
createdBy: user_id,
activityType: 'onboarding',
eventType: 'onboarding_issue_resolved',
});
resendOnboarding({ buyer_business, buyer_person, subscription, authAccountId })
Purpose: Resends onboarding form email to client contact.
Parameters:
buyer_business(ObjectId) - Business contact IDbuyer_person(ObjectId, optional) - Person contact IDsubscription(ObjectId) - Subscription IDauthAccountId(ObjectId) - Sending account ID
Returns: { success: true, message: 'SUCCESS' }
Validation Steps:
-
Validate Business Contact:
const bizCheck = await Contact.findOne({
_id: buyer_business,
$or: [
{ parent_account: authAccountId },
{ account: authAccountId }
]
});
if (!bizCheck) throw 404 error; -
Validate Person Contact (if provided):
const perCheck = await Contact.findOne({
_id: buyer_person,
$or: [
{ parent_account: authAccountId },
{ account: authAccountId }
],
businesses: bizCheck._id
});
if (!perCheck) throw 404 error; -
Fetch Order by subscription ID
-
Determine Recipient:
if (perCheck) {
recipient = { name: perCheck.name, email: perCheck.email, internal: true };
} else {
const acc_owner = await User.findOne({ account: authAccountId, is_owner: true });
recipient = { name: acc_owner.name, email: acc_owner.email, internal: true };
} -
Generate Form Link:
// Legacy form system
if (order.onboarding.form.form_secret_id) {
const activeDomain = await getActiveDomain({ accountId: authAccountId });
link = `${activeDomain}/forms/userform/${form_secret_id}?rid=${recipient_secret_id}&rqid=${request_id}`;
}
// Typeform system
else {
link = `${order.onboarding.typeform.link}#req_id=${order.onboarding.request}`;
} -
Queue Email:
const emailPayload = {
type: 'email',
origin: 'dashclicks',
content: {
template_id: 'd-5fba6541136d48b493bf1e80f88e08a4',
additional_data: {
form: form_name ?? 'Submit onboarding info',
link,
},
additional_message_data: {
from: 'no-reply@dashboardnotifications.com',
reply_to: { name: bizCheck.name, email: 'no-reply@dashboardnotifications.com' },
},
},
recipient,
sender_account: authAccountId,
check_credits: false,
internal_sender: forMainAcc,
};
await new NotificationQueue(emailPayload).save();
Data Flow Diagram
flowchart TD
A[GET Onboardings] --> B{Dashboard Enabled?}
B -->|Yes| C[Filter: sent/approved only]
B -->|No| D[No Status Filter]
C --> E[Match Active Subscriptions]
D --> E
E --> F[Lookup Typeform Responses]
F --> G[Lookup Requests]
G --> H[Extract Latest Onboarding Status]
H --> I[Find Latest Task]
I --> J[Determine Latest Event Type]
J --> K{Client Dashboard?}
K -->|Yes| L[Filter Sent Events by Metadata]
K -->|No| M[Lookup Contacts & Users]
L --> M
M --> N[Determine Target Recipient]
N --> O[Calculate Completed/Sent Dates]
O --> P{Dates Disabled?}
P -->|Yes| Q[Remove Date Fields]
P -->|No| R[Keep Date Fields]
Q --> S[Generate Typeform Link]
R --> S
S --> T[Apply Sorting]
T --> U[Facet: Data + Total]
U --> V[Return Onboardings]
W[UPDATE Response] --> X[Fetch Existing Response]
X --> Y[Build Field Map]
Y --> Z[Process Each Field Update]
Z --> AA{Multiple Choice?}
AA -->|Yes| AB[Handle Choices Array]
AA -->|No| AC[Update Value by Type]
AB --> AD[Collect Change Requests]
AC --> AD
AD --> AE[Update with Array Filters]
AE --> AF[Create Activity Log]
AF --> AG[Return Updated Response]
Integration Points
Internal Dependencies
MANAGED_SUBSCRIPTIONS- Service type filteringcreateActivityutility - Activity logginggetActiveDomainutility - Domain resolution for linksnotFounderror - 404 handling- SendGrid Template -
d-5fba6541136d48b493bf1e80f88e08a4
External Services
- Typeform - Form submissions and responses
- SendGrid - Email delivery via notification queue
Important Notes
- 🔄 Workflow States: sent → received → approved (linear progression)
- 🎯 Latest Event: Tasks override onboarding.status for event type
- 👤 Target Logic:
send_onboarding_to: 'me'sends to owner, else to contact - 📅 Date Scoping:
activity.onboarding_datesscope controls date visibility - 🔐 Client Filter: Only 'approved' and 'sent-to-client' visible on dashboard
- 📝 Typeform Types: 17+ field types with specific value key mappings
- ✅ Multiple Choice: Special handling for single vs multiple selections
- 🔗 Link Format: Typeform links append
#req_id=parameter - 📧 Email Template: Uses fixed SendGrid template for all resends
- 🏷️ Product Display: Shown as "Product Name (Tier)"
- ⚠️ Validation: Business and person contacts validated against account relationships
- 🔄 Array Filters: Dynamic array filter creation for nested answer updates
Related Documentation
- Subscriptions - Order context for onboardings
- Activity - Activity logging integration
- Dashboard - Dashboard preferences and scoping
- Projects Module Overview - Parent module
Last Updated: 2025-10-08
Service Files:services/onboardings.service.js,controllers/onboardings.controller.js
Primary Functions: 4 service functions (getOnboardings, getOrderOnboarding, updateOrderOnboardingService, resendOnboarding)