๐ Activity
๐ Overviewโ
The Activity service provides comprehensive event logging and audit trail functionality for the Projects module. It captures all significant actions, status changes, and milestones throughout the service delivery lifecycle, enabling detailed timeline views, compliance tracking, and client transparency.
Source Files:
- Service:
internal/api/v1/projects/services/activity.service.js - Controller:
internal/api/v1/projects/controllers/activity.controller.js
Key Capabilities:
- Retrieve paginated activity timelines for orders/subscriptions
- Support cursor-based pagination for infinite scroll
- Filter activities by type (onboarding, reports, work summaries, etc.)
- Apply scope-based access control for client dashboards
- Enrich activities with related data (communications, tasks, reports, users)
- Hide sensitive timestamp data based on dashboard preferences
๐๏ธ Collections Usedโ
๐ Full Schema: See Database Collections Documentation
activityโ
- Operations: Read (primary operations)
- Model:
shared/models/activity.js - Usage Context: Stores all project-related events with metadata
Key Fields:
{
_id: ObjectId,
type: 'projects', // Module identifier
ref_id: ObjectId, // Order/subscription reference
activity_type: String, // Event category
metadata: {
event_type: String, // Specific event
communication_id: ObjectId, // Linked communication
task_id: ObjectId, // Linked task
report_id: ObjectId, // Linked report
person_id: ObjectId // User/contact involved
},
created: Date, // Event timestamp
updated: Date
}
_store.ordersโ
- Operations: Read (order validation)
- Usage Context: Validates order access permissions
communicationsโ
- Operations: Read (enrich activities with message data)
- Usage Context: Links activities to communication threads
projects.tasksโ
- Operations: Read (enrich activities with task context)
- Usage Context: Links activities to task records
projects.reportsโ
- Operations: Read (enrich activities with report details)
- Usage Context: Links activities to service delivery reports
crm.contacts & _usersโ
- Operations: Read (person information)
- Usage Context: Identifies people involved in activities
๐ Data Flowโ
Activity Retrieval Flowโ
flowchart TD
A[๐ฏ API Request: GET /activity/:order_id] --> B{Dashboard Type?}
B -->|Agency Dashboard| C[Validate: seller_account = agency]
B -->|Client Dashboard/SSO| D[Validate: buyer_account in allowed list]
C --> E{Order Exists?}
D --> E
E -->|No| F[โ 404 Not Found]
E -->|Yes| G[Apply Activity Filters]
G --> H{Client Dashboard?}
H -->|Yes| I[Apply Scope Restrictions]
H -->|No| J[Full Access]
I --> K[Aggregate Activity Data]
J --> K
K --> L[Lookup Communications]
L --> M[Lookup Tasks]
M --> N[Lookup Reports]
N --> O[Lookup Person Info]
O --> P{Hide Timestamps?}
P -->|Yes| Q[Set hide_date flags]
P -->|No| R[Return All Data]
Q --> R
R --> S[๐ค Return Activity Timeline]
style A fill:#e1f5ff
style F fill:#ffe1e1
style S fill:#e1ffe1
๐ง Business Logic & Functionsโ
Service Layerโ
getActivity(options)โ
Purpose: Retrieves a paginated activity timeline for an order with rich contextual data, applying sophisticated scope-based filtering for client dashboard users.
Source: services/activity.service.js
Parameters:
orderId(ObjectId, required) - Target order IDaccountId(ObjectId, required) - Current account ID (for authorization)parentAccount(ObjectId, required) - Parent account IDdashboardPreferences(Object, optional) - Client dashboard settingsallow_client_dashboard(Boolean) - Client portal enabledsso(Boolean) - SSO authentication enabledparent_account(ObjectId) - Parent agency accountsub_account_ids(Array) - Allowed sub-account IDsscopes(Array) - Allowed activity scopes
limit(Number, required) - Number of activities to returnafter(ObjectId, optional) - Cursor for pagination (activity ID)filters(Object, optional) - Activity type filtersactivity_type(Array) - Specific activity types to include
Returns: Promise<Array>
[
{
id: ObjectId,
activity_type: String,
event_type: String,
created: Date,
updated: Date,
hide_date: Boolean, // Should timestamp be hidden?
metadata: Object,
// Enriched data (conditionally present):
message: {
// If communication exists
sent_by: ObjectId,
body: String,
type: String,
attachments: Array,
},
task: {
// If task exists
id: ObjectId,
status: String,
title: String,
type: String,
creator: String,
},
report: {
// If report exists
report_name: String,
report_type: String,
message: String,
files: Array,
link: String,
},
person: {
// If person exists
id: ObjectId,
name: String,
email: String,
phone: String,
image: String,
},
},
];
Business Logic Flow:
-
Order Access Validation
Determines query scope based on dashboard type:
const orderOptions = {
_id: orderId,
seller_account: parentAccount,
};
if (dashboardPreferences?.allow_client_dashboard || dashboardPreferences?.sso) {
orderOptions.buyer_account = {
$in: dashboardPreferences?.sub_account_ids?.map(id => new mongoose.Types.ObjectId(id)),
};
orderOptions.seller_account = dashboardPreferences.parent_account;
}Agency Users: Validates
seller_accountmatches Client Dashboard: Validatesbuyer_accountin allowed list -
Order Existence Check
const order = await StoreOrder.findOne(orderOptions, { _id: 1 }).lean();
if (!order) {
throw notFound('Order not found');
}Returns 404 if order doesn't exist or user lacks access.
-
Timestamp Restriction Logic
Determines which event types should have timestamps hidden for client users:
let restrictedEventTypes = [];
if (dashboardPreferences?.scopes?.length) {
const activityToggle = {
'activity.start_dates': ['subscription_created'],
'activity.onboarding_dates': ['onboarding_approved', 'onboarding_sent'],
};
const missingActivities = Object.keys(activityToggle).filter(
activity => !dashboardPreferences.scopes.includes(activity),
);
restrictedEventTypes = missingActivities.flatMap(activity => activityToggle[activity]);
}Purpose: Hides sensitive business data (subscription start dates, onboarding timing) if scope not granted.
-
Activity Type Filtering
Filters requested activity types against allowed scopes:
if (filters?.activity_type?.length) {
filters.activity_type = filters.activity_type.filter(activity =>
dashboardPreferences?.scopes?.includes(filterScopes[activity]),
);
}Uses scope mapping:
const filterScopes = {
order_status: 'onboardings',
onboarding: 'onboardings',
report: 'reports',
subscription_status: 'subscriptions',
work_summary: 'work-summary',
}; -
Base Query Construction
const options = {
type: 'projects',
ref_id: new mongoose.Types.ObjectId(orderId),
$and: [
{
$or: [
{ activity_type: { $ne: 'work_summary' } },
{
activity_type: 'work_summary',
created: { $gte: CLIENT_WORK_SUMMARY_LAUNCH_DATE },
},
],
},
makeFilters(filters),
],
};Work Summary Special Handling: Only shows work summaries created after launch date for clients.
-
Client Dashboard Activity Restrictions
Applies strict filtering for client users:
if (dashboardPreferences?.allow_client_dashboard || dashboardPreferences?.sso) {
const allowedActivities = ['report', 'onboarding', 'work_summary'];
const alwaysExcluded = ['approval', 'request'];
options['metadata.event_type'] = {
$nin: ['onboarding_received', 'onboarding_qa', 'onboarding_issues'],
};
const activitiesToExclude = allowedActivities.filter(
activity => !dashboardPreferences.scopes.includes(filterScopes[activity]),
);
alwaysExcluded.push(...activitiesToExclude);
if (alwaysExcluded.length > 0) {
options.activity_type = { $nin: alwaysExcluded };
}
}Always Hidden from Clients:
onboarding_received- Internal processing statusonboarding_qa- Internal QA reviewsonboarding_issues- Problem trackingapproval- Internal approval tasksrequest- Internal agency requests
-
Cursor-Based Pagination
if (after) {
options['_id'] = { $lt: new mongoose.Types.ObjectId(after) };
}Uses activity
_idas cursor for consistent pagination. -
MongoDB Aggregation Pipeline
Executes rich 10-stage aggregation:
[
{ $match: options },
{ $sort: { created: -1, _id: -1 } },
{ $limit: limit },
{
$lookup: {
from: 'communications',
localField: 'metadata.communication_id',
foreignField: '_id',
pipeline: [{ $project: { sent_by: 1, body: 1, type: 1, attachments: 1 } }],
as: 'message',
},
},
{
$lookup: {
from: 'projects.tasks',
localField: 'metadata.task_id',
foreignField: '_id',
as: 'task',
},
},
{
$lookup: {
from: 'projects.reports',
localField: 'metadata.report_id',
foreignField: '_id',
as: 'report',
},
},
{
$lookup: {
from: 'crm.contacts',
localField: 'metadata.person_id',
foreignField: '_id',
as: 'person',
},
},
{
$lookup: {
from: '_users',
localField: 'metadata.person_id',
foreignField: '_id',
as: 'user',
},
},
{
$addFields: {
message: { $first: '$message' },
task: { $first: '$task' },
report: { $first: '$report' },
person: { $ifNull: [{ $first: '$person' }, { $first: '$user' }] },
},
},
{
$project: {
message: 1,
report: 1,
task: 1,
person: 1,
activity_type: 1,
created: 1,
hide_date: {
$cond: {
if: { $gt: [{ $size: { $literal: restrictedEventTypes } }, 0] },
then: { $in: ['$metadata.event_type', { $literal: restrictedEventTypes }] },
else: false,
},
},
updated: 1,
id: '$_id',
event_type: '$metadata.event_type',
metadata: 1,
},
},
];Key Features:
- Enriches activities with related entity data
- Gracefully handles missing relationships (empty lookups)
- Applies
hide_dateflag for timestamp restrictions - Uses index hint for optimal performance
-
Index Optimization
.option({
hint: { type: 1, ref_id: 1, activity_type: 1, created: -1, _id: -1 }
});Suggests compound index usage for optimal query performance.
Key Business Rules:
- โ Scope-Based Filtering: Client users only see activities their scopes allow
- โ
Timestamp Hiding: Sensitive dates hidden via
hide_dateflag (UI responsibility) - โ Internal Activity Exclusion: Client users never see internal activities
- โ Work Summary Launch Date: Historical work summaries hidden from clients
- โ Order Access Validation: Validates access before returning any data
Error Handling:
404 Not Found: Order doesn't exist or user lacks access- Returns empty array
[]on database errors (graceful degradation)
Performance Notes:
-
Complex Aggregation: 10-stage pipeline with 5 lookups
-
Recommended Indexes:
activity: { type: 1, ref_id: 1, activity_type: 1, created: -1, _id: -1 }
activity: { 'metadata.communication_id': 1 }
activity: { 'metadata.task_id': 1 }
activity: { 'metadata.report_id': 1 }
activity: { 'metadata.person_id': 1 } -
Cursor Pagination: Efficient for infinite scroll scenarios
-
Lookup Optimization: Uses pipelines to minimize returned data
Example Usage:
const activities = await getActivity({
orderId: orderId,
accountId: accountId,
parentAccount: parentAccountId,
dashboardPreferences: {
allow_client_dashboard: true,
parent_account: parentAccountId,
sub_account_ids: [accountId],
scopes: ['reports', 'onboardings'],
},
limit: 20,
after: null, // First page
filters: {
activity_type: ['report', 'onboarding'],
},
});
console.log(activities.length); // Up to 20
console.log(activities[0].hide_date); // true/false based on scopes
Side Effects:
- ๐ Read-only: No data modifications
- โก Performance Impact: 100-400ms depending on data volume and enrichment
Controller Layerโ
getActivity(req, res)โ
Purpose: HTTP endpoint handler for activity retrieval. Parses query parameters, calls service layer, and formats response.
Source: controllers/activity.controller.js
Route: GET /api/v1/projects/orders/:order_id/activity
Request:
- URL Parameters:
order_id(ObjectId) - Target order ID
- Query Parameters:
limit(Number, required) - Number of activities to returnafter(ObjectId, optional) - Cursor for paginationactivity_type(String, optional) - Comma-separated activity types
Response:
-
Success (200):
{
success: true,
message: 'SUCCESS',
data: [...] // Array of activity objects
} -
Error (404): Not Found - Order doesn't exist or no access
Logic:
-
Parameter Parsing
let { after, limit, activity_type } = req.query;
const { order_id: orderId } = req.params;
const dashboardPreferences = req.auth.dashboard_preferences;
const accountId = req.auth.account_id;
limit = parseInt(limit); -
Activity Type Splitting
filters: {
...(activity_type && { activity_type: activity_type.split(',') }),
}Converts comma-separated string to array:
"report,onboarding"โ['report', 'onboarding'] -
Service Call
const activityData = await activityService.getActivity({
orderId,
accountId,
limit: limit,
parentAccount: req.auth.account_id,
dashboardPreferences,
after,
filters,
});
Example Request:
GET /api/v1/projects/orders/507f1f77bcf86cd799439011/activity?limit=20&activity_type=report,onboarding
Authorization: Bearer <jwt_token>
Example Response:
{
"success": true,
"message": "SUCCESS",
"data": [
{
"id": "507f1f77bcf86cd799439012",
"activity_type": "report",
"event_type": "report_uploaded",
"created": "2025-10-05T14:30:00Z",
"hide_date": false,
"report": {
"report_name": "September SEO Performance",
"report_type": "monthly",
"files": ["https://..."]
},
"person": {
"id": "507f1f77bcf86cd799439013",
"name": "John Smith",
"email": "john@example.com"
}
}
]
}
๐ Integration Pointsโ
Internal Dependenciesโ
CLIENT_WORK_SUMMARY_LAUNCH_DATE(utilities/constants.js) - Feature launch date constantcatchAsync()(utilities/catch-async.js) - Error handling wrappernotFound()(utilities/catch-errors.js) - 404 error constructor- Store Module - Order validation
- Projects Module - Tasks, reports, communications linkage
External Servicesโ
None - Pure internal data operations
๐งช Edge Cases & Special Handlingโ
Case: Client User Requesting Internal Activitiesโ
Condition: Client dashboard user requests activity types like 'approval' or 'request'
Handling:
const alwaysExcluded = ['approval', 'request'];
options.activity_type = { $nin: alwaysExcluded };
Internal activities are always filtered out for client users.
Case: Missing Scope for Requested Activity Typeโ
Condition: User filters by activity_type: ['report'] but lacks 'reports' scope
Handling:
filters.activity_type = filters.activity_type.filter(activity =>
dashboardPreferences?.scopes?.includes(filterScopes[activity]),
);
Filtered activity types are removed from query - returns empty if all filtered out.
Case: Activity with Non-Existent Relationshipsโ
Condition: Activity references deleted task/report/communication
Handling:
{
$lookup: { ... },
$addFields: {
task: { $first: '$task' } // Will be null if lookup returns empty
}
}
Gracefully returns null for missing relationships. UI should handle null values.
Case: Historical Work Summaries for Clientโ
Condition: Work summaries exist before CLIENT_WORK_SUMMARY_LAUNCH_DATE
Handling:
{
$or: [
{ activity_type: { $ne: 'work_summary' } },
{
activity_type: 'work_summary',
created: { $gte: CLIENT_WORK_SUMMARY_LAUNCH_DATE },
},
];
}
Only work summaries after launch date are visible to clients.
Case: Timestamp Hidingโ
Condition: Client user lacks 'activity.start_dates' scope
Handling:
hide_date: {
$cond: {
if: { $in: ['$metadata.event_type', ['subscription_created']] },
then: true,
else: false,
},
}
Flag is set, but timestamp still returned - UI must check hide_date and hide accordingly.
โ ๏ธ Important Notesโ
- ๐ Scope Enforcement Critical: Client dashboard filtering prevents exposure of sensitive internal activities
- ๐ Timestamp Hiding is UI Responsibility:
hide_dateflag returned - UI must respect it - โก Cursor Pagination: Use
afterparameter with last activity_idfor next page - ๐ Activity Type Mapping: Use
filterScopesmapping to determine scope requirements - ๐ Launch Date Filtering: Work summaries have historical cutoff for client visibility
- ๐ก Null Relationships: Activities may have null
task,report,message, orpersonfields
๐ Related Documentationโ
- Task Management - Tasks linked in activities
- Reports - Reports linked in activities
- Onboardings - Onboarding events in timeline
- Projects Module Overview - Parent module architecture
Last Updated: 2025-10-08 Service Files:
services/activity.service.js,controllers/activity.controller.js> Primary Functions: 1 service function, 1 controller endpoint