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