Skip to main content

๐Ÿ“— 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 ID
  • accountId (ObjectId, required) - Current account ID (for authorization)
  • parentAccount (ObjectId, required) - Parent account ID
  • dashboardPreferences (Object, optional) - Client dashboard settings
    • allow_client_dashboard (Boolean) - Client portal enabled
    • sso (Boolean) - SSO authentication enabled
    • parent_account (ObjectId) - Parent agency account
    • sub_account_ids (Array) - Allowed sub-account IDs
    • scopes (Array) - Allowed activity scopes
  • limit (Number, required) - Number of activities to return
  • after (ObjectId, optional) - Cursor for pagination (activity ID)
  • filters (Object, optional) - Activity type filters
    • activity_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:

  1. 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_account matches Client Dashboard: Validates buyer_account in allowed list

  2. 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.

  3. 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.

  4. 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',
    };
  5. 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.

  6. 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 status
    • onboarding_qa - Internal QA reviews
    • onboarding_issues - Problem tracking
    • approval - Internal approval tasks
    • request - Internal agency requests
  7. Cursor-Based Pagination

    if (after) {
    options['_id'] = { $lt: new mongoose.Types.ObjectId(after) };
    }

    Uses activity _id as cursor for consistent pagination.

  8. 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_date flag for timestamp restrictions
    • Uses index hint for optimal performance
  9. 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_date flag (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 return
    • after (ObjectId, optional) - Cursor for pagination
    • activity_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:

  1. 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);
  2. Activity Type Splitting

    filters: {
    ...(activity_type && { activity_type: activity_type.split(',') }),
    }

    Converts comma-separated string to array: "report,onboarding" โ†’ ['report', 'onboarding']

  3. 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 constant
  • catchAsync() (utilities/catch-async.js) - Error handling wrapper
  • notFound() (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_date flag returned - UI must respect it
  • โšก Cursor Pagination: Use after parameter with last activity _id for next page
  • ๐Ÿ” Activity Type Mapping: Use filterScopes mapping to determine scope requirements
  • ๐Ÿ“… Launch Date Filtering: Work summaries have historical cutoff for client visibility
  • ๐Ÿ’ก Null Relationships: Activities may have null task, report, message, or person fields


Last Updated: 2025-10-08 Service Files: services/activity.service.js, controllers/activity.controller.js > Primary Functions: 1 service function, 1 controller endpoint

๐Ÿ’ฌ

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