๐ 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 objectaccountInfo.account_id(ObjectId) - Current account IDaccountInfo.userId(ObjectId) - Current user IDaccountInfo.isOwner(Boolean) - Whether user is account owneraccountInfo.dashboardPreferences(Object) - SSO/client dashboard settingsdashboardPreferences.allow_client_dashboard(Boolean) - Client portal enableddashboardPreferences.sso(Boolean) - SSO authentication enableddashboardPreferences.parent_account(ObjectId) - Parent agency accountdashboardPreferences.sub_account_ids(Array) - Allowed sub-account IDs
search(String, optional) - Search term for business name or phoneskip(Number) - Pagination offsetlimit(Number) - Max results to returnactive(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:
-
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
-
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).
-
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_projectssetting enabled, forces active filter even if not requested. -
MongoDB Aggregation Pipeline
Executes complex 6-stage aggregation:
Stage 1: Match Base Filters
{
$match: options;
} // Apply access control filtersStage 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
activeflag set, requires at least ONE active subscription
-
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 firstbusiness.name: 1โ Alphabetical by business name
-
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:
-
MongoDB Aggregation Pipeline
Executes similar pipeline to
getAccountsbut for single account:[
{ $match: { _id: new mongoose.Types.ObjectId(accountId) } },
// ... same lookups as getAccounts ...
{
$project: {
/* same projection + parent_account */
},
},
]; -
Parent Account Inclusion
Unlike
getAccounts, this function includesparent_accountin response:{
$project: {
parent_account: 1, // Exposed for authorization
// ... other fields
}
}This enables controller to verify user has access to account.
-
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
undefinedif 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 pagesearch(String, optional) - Search termactive(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:
-
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); -
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.
-
Service Call
const { data: accountData, total } = await accountService.getAccounts({
limit,
skip,
search,
accountInfo,
active,
}); -
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:
-
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.
-
Fetch Account
const account = await accountService.getAccount({ accountId });
if (!account) {
throw notFound('Account not found');
} -
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. -
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 typesgeneratePagination()(utilities/index.js) - Pagination helpercatchAsync()(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_idsto 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:
getAccountreturns parent_account,getAccountsdoesn't - inconsistent but intentional
๐ Related Documentationโ
- Projects Module Overview - Parent module architecture
- Subscriptions - Subscription status details
- Dashboard - Dashboard aggregation using accounts
Last Updated: 2025-10-08 Service Files:
services/accounts.service.js,controllers/accounts.controller.js> Primary Functions: 2 service functions, 2 controller endpoints