Skip to main content

๐Ÿ“˜ Accounts

๐Ÿ“– Overviewโ€‹

The Accounts service provides filtered sub-account listing for the Projects module, enabling agencies to view and manage their client accounts within project contexts. It supports sophisticated filtering for white-labeled client portals with SSO integration and granular permission scoping.

Source Files:

  • Service: internal/api/v1/projects/services/accounts.service.js
  • Controller: internal/api/v1/projects/controllers/accounts.controller.js

Key Capabilities:

  • Retrieve all sub-accounts with managed service subscriptions
  • Support SSO-based client dashboard access with scoped filtering
  • Search across account name, business information, and contact details
  • Filter active/inactive subscriptions based on user preferences
  • Provide detailed account context including business profile and subscription status

๐Ÿ—„๏ธ Collections Usedโ€‹

๐Ÿ“š Full Schema: See Database Collections Documentation

_accountsโ€‹

  • Operations: Read (primary collection)
  • Model: shared/models/account.js
  • Usage Context: Base account data, parent-child relationships, main account flags

crm.contactsโ€‹

  • Operations: Read (business profile enrichment)
  • Model: shared/models/crm-contact.js
  • Usage Context: Business name, contact information, logo/images, address details

_store.subscriptionsโ€‹

  • Operations: Read (active subscription filtering)
  • Model: shared/models/store-subscription.js
  • Usage Context: Subscription status validation, managed service filtering

user-configโ€‹

  • Operations: Read (user preferences)
  • Model: shared/models/user-config.js
  • Usage Context: Hide inactive projects preference retrieval

projects-dashboard-preferencesโ€‹

  • Operations: Read (SSO configuration)
  • Model: shared/models/projects-dashboard-preferences.js
  • Usage Context: Client dashboard scope, allowed sub-accounts, parent account mapping

๐Ÿ”„ Data Flowโ€‹

Account Listing Flowโ€‹

flowchart TD
A[๐ŸŽฏ API Request: GET /accounts] --> B{Dashboard Type?}
B -->|Agency Dashboard| C[Filter: parent_account = agency_id]
B -->|Client Dashboard/SSO| D[Filter: sub_account_ids from preferences]

C --> E[Optional: Check User Config]
D --> E

E --> F{Hide Inactive Setting?}
F -->|Yes| G[Filter: active subscriptions only]
F -->|No| H[Include all subscriptions]

G --> I[Aggregate Account Data]
H --> I

I --> J[Lookup Business Profile]
J --> K[Lookup Subscription Status]
K --> L{Search Term?}
L -->|Yes| M[Apply Search Filter]
L -->|No| N[Sort by main flag + name]

M --> N
N --> O[Paginate Results]
O --> P[๐Ÿ“ค Return Account List]

style A fill:#e1f5ff
style P fill:#e1ffe1

๐Ÿ”ง Business Logic & Functionsโ€‹

Service Layerโ€‹


getAccounts(options)โ€‹

Purpose: Retrieves a paginated list of sub-accounts with rich context including business profiles, subscription status, and SSO user information. This is the primary account listing function for project views, supporting both agency dashboards and white-labeled client portals.

Source: services/accounts.service.js

Parameters:

  • accountInfo (Object, required) - Account context object
    • accountInfo.account_id (ObjectId) - Current account ID
    • accountInfo.userId (ObjectId) - Current user ID
    • accountInfo.isOwner (Boolean) - Whether user is account owner
    • accountInfo.dashboardPreferences (Object) - SSO/client dashboard settings
      • dashboardPreferences.allow_client_dashboard (Boolean) - Client portal enabled
      • dashboardPreferences.sso (Boolean) - SSO authentication enabled
      • dashboardPreferences.parent_account (ObjectId) - Parent agency account
      • dashboardPreferences.sub_account_ids (Array) - Allowed sub-account IDs
  • search (String, optional) - Search term for business name or phone
  • skip (Number) - Pagination offset
  • limit (Number) - Max results to return
  • active (Boolean, optional) - Filter for active subscriptions only

Returns: Promise<Object>

{
data: [
{
id: ObjectId,
business: {
id: ObjectId,
name: String,
email: String,
phone: String,
logo: String,
images: Array,
address: Object
},
main: Boolean, // Is primary account
currency: String,
became_customer_on: Date,
created_at: Date,
updated_at: Date,
hasActiveSubscription: Boolean,
user: { // SSO only
id: ObjectId,
name: String
}
}
],
total: [
{ total: Number }
]
}

Business Logic Flow:

  1. Access Control Determination

    Determines query scope based on dashboard type:

    const isClientDashboard = dashboardPreferences?.allow_client_dashboard;
    const isSSO = dashboardPreferences?.sso;

    const options =
    isClientDashboard || isSSO
    ? {
    active: true,
    parent_account: dashboardPreferences.parent_account,
    _id: { $in: dashboardPreferences.sub_account_ids },
    }
    : {
    active: true,
    $or: [
    { parent_account: account_id },
    { _id: account_id }, // Include main account
    ],
    };

    Agency Dashboard: Shows all sub-accounts plus main account Client Dashboard/SSO: Shows only explicitly allowed sub-accounts

  2. Search Implementation

    When search term provided, performs regex matching:

    const searchMatch = search
    ? {
    $or: [
    { 'business.name': { $regex: search.toString(), $options: 'i' } },
    { 'business.phone': { $regex: search.toString(), $options: 'i' } },
    ],
    }
    : {};

    Matches against business name and phone number (case-insensitive).

  3. User Preference Check

    Fetches user's project preferences to determine if inactive projects should be hidden:

    if (!active) {
    try {
    const config = await UserConfig.findOne({
    user_id: userId,
    type: 'projects',
    })
    .lean()
    .exec();
    if (config?.preferences?.projects?.hide_inactive_projects) {
    active = true; // Override to hide inactive
    }
    } catch (error) {
    logger.error({ error });
    // Fail silently, proceed with original active value
    }
    }

    If user has hide_inactive_projects setting enabled, forces active filter even if not requested.

  4. MongoDB Aggregation Pipeline

    Executes complex 6-stage aggregation:

    Stage 1: Match Base Filters

    {
    $match: options;
    } // Apply access control filters

    Stage 2-3: Lookup Business Profile

    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'business',
    foreignField: '_id',
    as: 'business'
    }
    },
    { $unwind: { path: '$business', preserveNullAndEmptyArrays: true } }

    Joins account to CRM contact for business name, logo, contact info.

    Stage 4: Optional SSO User Lookup

    If SSO enabled, fetches account owner user:

    {
    $lookup: {
    from: '_users',
    localField: '_id',
    foreignField: 'account',
    as: 'user',
    pipeline: [
    { $match: { is_owner: true, active: true } },
    { $limit: 1 },
    { $project: { id: '$_id', _id: 0, name: 1 } }
    ]
    }
    }

    Stage 5: Subscription Status Validation

    Critical stage that determines subscription status:

    {
    $lookup: {
    from: '_store.subscriptions',
    let: { id: '$_id' },
    pipeline: [
    {
    $match: {
    $expr: {
    $and: [
    { $eq: ['$metadata.account_id', '$$id'] },
    { $in: ['$plan.metadata.product_type', MANAGED_SUBSCRIPTIONS] }
    ]
    }
    }
    },
    {
    $project: {
    _id: 0,
    status: 1,
    product_type: '$plan.metadata.product_type'
    }
    },
    {
    $facet: {
    status: [
    {
    $group: {
    _id: null,
    status: {
    $sum: {
    $cond: [{ $eq: ['$status', 'active'] }, 1, 0]
    }
    }
    }
    }
    ],
    hasSubscription: [{ $count: 'total' }]
    }
    },
    {
    $set: {
    hasActiveSubscription: {
    $cond: [{ $gt: [{ $first: '$status.status' }, 0] }, true, false]
    },
    hasSubscription: {
    $cond: [
    { $gt: [{ $first: '$hasSubscription.total' }, 0] },
    true,
    false,
    ]
    }
    }
    }
    ],
    as: 'subscription'
    }
    }

    This nested pipeline:

    • Finds all managed service subscriptions for account
    • Counts total subscriptions (hasSubscription)
    • Counts active subscriptions (hasActiveSubscription)
    • Returns boolean flags for both

    Stage 6: Filter by Subscription Status

    {
    $match: {
    'subscription.hasSubscription': true,
    ...(active ? { 'subscription.hasActiveSubscription': true } : {})
    }
    }
    • Always requires at least ONE managed subscription
    • If active flag set, requires at least ONE active subscription
  5. Pagination & Sorting

    Final facet for data + count:

    {
    $facet: {
    data: [
    { $sort: { main: -1, 'business.name': 1 } }, // Main account first, then alphabetical
    { $skip: skip },
    { $limit: limit }
    ],
    total: [{ $count: 'total' }]
    }
    }

    Sorts by:

    • main: -1 โ†’ Main account appears first
    • business.name: 1 โ†’ Alphabetical by business name
  6. Response Shaping

    Projects response with clean structure:

    {
    $project: {
    id: '$_id',
    _id: 0,
    business: {
    id: '$business._id',
    name: 1,
    email: 1,
    phone: 1,
    logo: '$business.image',
    images: 1,
    address: 1
    },
    main: { $ifNull: ['$main', false] },
    currency: 1,
    became_customer_on: 1,
    created_at: 1,
    updated_at: 1,
    hasActiveSubscription: { $first: '$subscription.hasActiveSubscription' },
    user: { $first: '$user' } // SSO only
    }
    }

Key Business Rules:

  • โœ… Managed Services Only: Only shows accounts with at least one managed service subscription
  • โœ… SSO Scoping: Client dashboard users only see explicitly allowed sub-accounts
  • โœ… Main Account Inclusion: Agency view includes the parent account itself in results
  • โœ… Active Filter Enforcement: User preference can override requested active filter
  • โœ… Business Profile Required: Accounts without CRM contacts still appear (null business)

Error Handling:

  • Wrapped in try/catch with Promise.reject(error) propagation
  • User config fetch failures are logged but don't block execution
  • Returns {} on aggregation failure (graceful degradation)

Performance Notes:

  • Complex Aggregation: 6-stage pipeline with nested subscription lookup
  • Recommended Indexes:
    _accounts: { parent_account: 1, active: 1, main: 1 }
    crm.contacts: { _id: 1 }
    _store.subscriptions: { 'metadata.account_id': 1, status: 1, 'plan.metadata.product_type': 1 }
    _users: { account: 1, is_owner: 1, active: 1 }
  • Always use pagination to avoid loading hundreds of accounts

Example Usage:

const accounts = await getAccounts({
accountInfo: {
account_id: agencyAccountId,
userId: currentUserId,
isOwner: true,
dashboardPreferences: null, // Agency user
},
search: 'dentist',
skip: 0,
limit: 20,
active: true, // Only active subscriptions
});

console.log(accounts.data.length); // 15 accounts
console.log(accounts.total[0].total); // 15 total matches

Side Effects:

  • ๐Ÿ“– Read-only: No data modifications
  • ๐Ÿ” User Config Query: May query user preferences (read-only)
  • โšก Performance Impact: 100-300ms on large datasets

getAccount(options)โ€‹

Purpose: Retrieves a single account with full details including business profile, subscription status, and parent account reference. Used for account detail views and validation.

Source: services/accounts.service.js

Parameters:

  • accountId (ObjectId, required) - Target account ID

Returns: Promise<Object>

{
id: ObjectId,
parent_account: ObjectId, // Included for authorization checks
business: {
id: ObjectId,
name: String,
email: String,
phone: String,
logo: String,
images: Array,
address: Object
},
main: Boolean,
currency: String,
became_customer_on: Date,
created_at: Date,
updated_at: Date,
hasActiveSubscription: Boolean
}

Business Logic Flow:

  1. MongoDB Aggregation Pipeline

    Executes similar pipeline to getAccounts but for single account:

    [
    { $match: { _id: new mongoose.Types.ObjectId(accountId) } },
    // ... same lookups as getAccounts ...
    {
    $project: {
    /* same projection + parent_account */
    },
    },
    ];
  2. Parent Account Inclusion

    Unlike getAccounts, this function includes parent_account in response:

    {
    $project: {
    parent_account: 1, // Exposed for authorization
    // ... other fields
    }
    }

    This enables controller to verify user has access to account.

  3. Return Single Document

    const account = await Accounts.aggregate([...]);
    return account[0]; // Returns first/only result

Key Business Rules:

  • โœ… No Access Control: Function doesn't filter by parent account (controller handles this)
  • โœ… Parent Account Exposed: Returns parent_account for authorization checks
  • โœ… Same Subscription Logic: Uses identical subscription validation as getAccounts

Error Handling:

  • Returns undefined if account not found (controller handles 404)
  • Propagates aggregation errors

Example Usage:

const account = await getAccount({
accountId: subAccountId,
});

if (!account) {
throw notFound('Account not found');
}

if (account.parent_account.toString() !== authAccountId) {
throw notAuthorized('Access denied');
}

Side Effects:

  • ๐Ÿ“– Read-only: No modifications
  • โšก Performance: ~50-150ms single document aggregation

Controller Layerโ€‹


getAccounts(req, res)โ€‹

Purpose: HTTP endpoint handler for account listing. Validates request, calls service layer, and formats paginated response.

Source: controllers/accounts.controller.js

Route: GET /api/v1/projects/accounts

Request:

  • Query Parameters:
    • page (Number, optional) - Page number (1-indexed)
    • limit (Number, required) - Results per page
    • search (String, optional) - Search term
    • active (Boolean, optional) - Filter active subscriptions

Response:

  • Success (200):
    {
    success: true,
    message: 'SUCCESS',
    data: [...], // Array of account objects
    pagination: {
    total: Number,
    page: Number,
    limit: Number,
    totalPages: Number
    }
    }

Logic:

  1. Request Parsing

    let { page, limit, search, active } = req.query;
    if (search) {
    search = search.replace('+', ''); // Remove + from phone numbers
    search = decodeURI(search); // Decode special chars
    }
    limit = parseInt(limit);
    page = page ? parseInt(page) : 0;
    const skip = Math.max(0, (page - 1) * limit);
  2. Account Info Assembly

    const accountInfo = {
    account_id: req.auth.account_id,
    isOwner: req.auth.user.is_owner,
    userId: req.auth.uid,
    dashboardPreferences: req.auth.dashboard_preferences,
    };

    Extracts authentication context from JWT.

  3. Service Call

    const { data: accountData, total } = await accountService.getAccounts({
    limit,
    skip,
    search,
    accountInfo,
    active,
    });
  4. Pagination Generation

    const totalRecords = total?.[0]?.total || 0;
    res.json({
    success: true,
    message: 'SUCCESS',
    data: accountData,
    pagination: generatePagination(limit, page, totalRecords),
    });

Example Request:

GET /api/v1/projects/accounts?page=1&limit=20&search=dental&active=true
Authorization: Bearer <jwt_token>

Example Response:

{
"success": true,
"message": "SUCCESS",
"data": [
{
"id": "507f1f77bcf86cd799439011",
"business": {
"id": "507f1f77bcf86cd799439012",
"name": "Smith Dental Clinic",
"email": "info@smithdental.com",
"phone": "555-0123",
"logo": "https://...",
"address": { "city": "Austin", "state": "TX" }
},
"main": false,
"currency": "usd",
"hasActiveSubscription": true,
"created_at": "2024-01-15T08:00:00Z"
}
],
"pagination": {
"total": 1,
"page": 1,
"limit": 20,
"totalPages": 1
}
}

getAccount(req, res)โ€‹

Purpose: HTTP endpoint handler for single account retrieval with authorization checks.

Source: controllers/accounts.controller.js

Route: GET /api/v1/projects/accounts/:account_id

Request:

  • URL Parameters:
    • account_id (ObjectId) - Target account ID

Response:

  • Success (200):
    {
    data: {
    id: ObjectId,
    business: { ... },
    main: Boolean,
    hasActiveSubscription: Boolean,
    // ... other fields (no parent_account)
    }
    }
  • Error (403): Forbidden - User doesn't have access
  • Error (404): Not Found - Account doesn't exist

Logic:

  1. Authorization Check for Client Dashboard Users

    if (
    !req.auth.account?.main &&
    !dashboardPreferences?.sub_account_ids?.some(id => id.equals(accountId))
    ) {
    throw forbidden('You are not allowed to access this resource');
    }

    Client dashboard users can only access accounts in their allowed list.

  2. Fetch Account

    const account = await accountService.getAccount({ accountId });
    if (!account) {
    throw notFound('Account not found');
    }
  3. Authorization Check for Regular Users

    if (!req.auth.user.dashclicks?.general) {
    // Regular users can access:
    // - Their own account
    // - Sub-accounts of their parent account
    // - Accounts in their dashboard preferences
    if (
    authAccountId !== accountId &&
    authAccountId !== account.parent_account?.toString() &&
    !dashboardPreferences?.sub_account_ids?.some(id => id.equals(accountId))
    ) {
    throw notAuthorized('You have no access to this account.');
    }
    }

    Platform admins (dashclicks.general: true) bypass this check.

  4. Remove Parent Account from Response

    delete account.parent_account; // Keep consistent with /accounts endpoint
    res.json({ data: account });

Key Business Rules:

  • ๐Ÿ”’ Multi-layer Authorization: Checks both JWT auth and account relationships
  • ๐Ÿ”’ Client Dashboard Scoping: Client users limited to allowed sub-accounts
  • ๐Ÿ”’ Admin Bypass: Platform admins can access any account
  • ๐Ÿ”’ Parent Account Validation: Regular users must belong to parent or be in allowed list

Example Request:

GET /api/v1/projects/accounts/507f1f77bcf86cd799439011
Authorization: Bearer <jwt_token>

Example Response:

{
"data": {
"id": "507f1f77bcf86cd799439011",
"business": {
"name": "Smith Dental Clinic",
"email": "info@smithdental.com"
},
"main": false,
"hasActiveSubscription": true
}
}

๐Ÿ”€ Integration Pointsโ€‹

Internal Dependenciesโ€‹

  • MANAGED_SUBSCRIPTIONS (utilities/admin-filters.js) - List of managed service types
  • generatePagination() (utilities/index.js) - Pagination helper
  • catchAsync() (utilities/catch-async.js) - Error handling wrapper
  • Accounts Module - Account hierarchy and relationships
  • Store Module - Subscription status validation
  • CRM Module - Business profile data

External Servicesโ€‹

None - Pure internal data operations


๐Ÿงช Edge Cases & Special Handlingโ€‹

Case: Account Without Business Profileโ€‹

Condition: Account exists but business field is null or references non-existent contact

Handling:

{ $unwind: { path: '$business', preserveNullAndEmptyArrays: true } }

Account appears in results with business: null. UI should handle gracefully.

Case: Search with Phone Number Plus Signโ€‹

Condition: User searches for "+1-555-0123"

Handling:

search = search.replace('+', ''); // Remove + before regex

Prevents regex interpretation of + as quantifier.

Case: SSO User with Empty Allowed Listโ€‹

Condition: dashboardPreferences.sub_account_ids = []

Handling:

options._id = { $in: [] }; // Matches no accounts

Returns empty result set - user sees no accounts (correct behavior for misconfigured SSO).

Case: User Preference Overrideโ€‹

Condition: User has hide_inactive_projects enabled but active not requested

Handling:

if (config?.preferences?.projects?.hide_inactive_projects) {
active = true; // Force active filter
}

User preference takes precedence over API request.


โš ๏ธ Important Notesโ€‹

  • ๐Ÿ”’ Data Isolation: SSO users MUST be filtered by sub_account_ids to prevent data leakage
  • ๐Ÿ“Š Subscription Requirement: Only accounts with managed subscriptions appear (no self-service accounts)
  • ๐Ÿ’ก Main Account: Agency dashboard includes parent account itself in results
  • โšก Performance: Pagination is essential - never load all accounts without limits
  • ๐Ÿ” Search Limitations: Regex search on large datasets can be slow - consider full-text search for scale
  • ๐Ÿ› Parent Account Exposure: getAccount returns parent_account, getAccounts doesn't - inconsistent but intentional


Last Updated: 2025-10-08 Service Files: services/accounts.service.js, controllers/accounts.controller.js > Primary Functions: 2 service functions, 2 controller endpoints

๐Ÿ’ฌ

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