Skip to main content

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) - Contains account_id and dashboardPreferences
  • skip (Number) - Pagination offset
  • limit (Number) - Page size
  • sortby (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 ID
  • accountId (ObjectId) - Seller account ID
  • dashboardPreferences (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 ID
  • payload (Array) - Array of field updates with values and change requests
  • account_id (ObjectId) - Account performing update
  • user_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:

  1. Fetch Existing Response and build field definition map

  2. 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;
    }
    });
  3. Update with Array Filters:

    const arrayFilters = payload.map((item, index) => ({
    [`answer${index}.field.id`]: item.id,
    }));

    await onboardingTypeformResponse.updateOne(
    { _id: responseId },
    { $set: updateFields },
    { arrayFilters },
    );
  4. 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 ID
  • buyer_person (ObjectId, optional) - Person contact ID
  • subscription (ObjectId) - Subscription ID
  • authAccountId (ObjectId) - Sending account ID

Returns: { success: true, message: 'SUCCESS' }

Validation Steps:

  1. Validate Business Contact:

    const bizCheck = await Contact.findOne({
    _id: buyer_business,
    $or: [
    { parent_account: authAccountId },
    { account: authAccountId }
    ]
    });
    if (!bizCheck) throw 404 error;
  2. 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;
  3. Fetch Order by subscription ID

  4. 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 };
    }
  5. 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}`;
    }
  6. 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 filtering
  • createActivity utility - Activity logging
  • getActiveDomain utility - Domain resolution for links
  • notFound error - 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_dates scope 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

Last Updated: 2025-10-08
Service Files: services/onboardings.service.js, controllers/onboardings.controller.js
Primary Functions: 4 service functions (getOnboardings, getOrderOnboarding, updateOrderOnboardingService, resendOnboarding)

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM