Skip to main content

Task Management (Request/Approval)

πŸ“– Overview​

The Task Management service is the communication backbone of the Projects module, enabling asynchronous collaboration between platform admins, agency partners, and end clients throughout the service delivery lifecycle. It manages the full task workflowβ€”from creation and assignment through threaded communication, status tracking, and resolutionβ€”while maintaining comprehensive audit trails and real-time notifications.

File Path: internal/api/v1/projects/services/task.service.js (2066 lines)

Why Tasks Exist​

Digital marketing service delivery is inherently asynchronous and multi-party:

  • Onboarding Issues: Client submissions need clarification or corrections
  • Payment Problems: Past-due subscriptions require agency attention
  • Client Requests: Agencies need to communicate needs to platform
  • Approval Workflows: Platform decisions require agency input
  • Setup Coordination: Service provisioning involves multiple stakeholders

Tasks provide a structured, trackable, auditable communication channel that replaces ad-hoc emails and enables automated workflow management.

πŸ—„οΈ Collections Used​

πŸ“š Full Schema: See Database Collections Documentation

projects.tasks​

  • Operations: Create, Read, Update, Delete
  • Model: shared/models/projects-tasks.js
  • Usage Context: Primary collection for task storage, status management, and lifecycle tracking

Key Fields:

{
_id: ObjectId,
title: String, // Task summary
type: String, // Task category (enum)
status: 'pending' | 'completed',
creator: 'dashclicks' | String, // Origin identifier
account_id: ObjectId, // Client account
parent_account: ObjectId, // Agency account
order_id: ObjectId, // Related order
removed: Boolean, // Soft delete flag
created_at: Date,
updated_at: Date
}

communications​

  • Operations: Create, Read
  • Model: shared/models/communication.js
  • Usage Context: Stores threaded replies/messages within tasks

Key Fields:

{
task_id: ObjectId, // Parent task
type: 'OUTGOING' | 'INCOMING', // Direction
message: String, // Message content
attachments: Array, // File references
user_id: ObjectId, // Author
created_at: Date
}

_store.orders​

  • Operations: Read (lookup for product/subscription context)
  • Usage Context: Links tasks to service orders for context enrichment

_store.subscriptions​

  • Operations: Read (active subscription validation)
  • Usage Context: Determines if service is still active, cancellation status

_accounts​

  • Operations: Read (sub-account details)
  • Usage Context: Populates client account information in task views

_users​

  • Operations: Read (assigned user details)
  • Usage Context: Shows responsible team members for tasks

activity​

  • Operations: Create (activity logging)
  • Usage Context: Logs task creation, status changes, replies for audit trail

onboarding.typeform.requests​

  • Operations: Read (onboarding form status)
  • Usage Context: Enriches onboarding-related tasks with form send dates

πŸ”„ Data Flow​

Task Creation Flow​

flowchart TD
A[🎯 Trigger Event] --> B{Event Type?}
B -->|Onboarding Issue| C[Create onboarding_issues task]
B -->|Payment Failed| D[Create subscription_past_due task]
B -->|Client Request| E[Create request task]
B -->|Setup Needed| F[Create setup task]

C --> G[Store in projects.tasks]
D --> G
E --> G
F --> G

G --> H[Log Activity]
H --> I[Emit Socket Event]
I --> J[Send Email Notification]
J --> K[Task Created βœ“]

style A fill:#e1f5ff
style K fill:#e1ffe1

Communication Thread Flow​

sequenceDiagram
participant Agency as Agency User
participant API as Task API
participant DB as MongoDB
participant Socket as Socket.IO
participant Platform as Platform User

Agency->>API: POST /tasks/:id/reply
API->>DB: Create communication doc
API->>DB: Update task.updated_at
API->>DB: Log activity
DB-->>API: Confirm
API->>Socket: Emit 'task_reply' event
Socket-->>Platform: Real-time notification
Socket-->>Agency: Confirmation
API-->>Agency: Reply saved response

Pending Action Aggregation Flow​

flowchart TD
A[GET /tasks/pending-action] --> B[Filter by parent_account]
B --> C[Exclude: setup, onboarding_qa, work_summary]
C --> D[Lookup Sub-Account Details]
D --> E[Lookup Order + Subscription]
E --> F[Lookup Assigned Users]
F --> G[Calculate Days Pending]
G --> H{Has Search Term?}
H -->|Yes| I[Apply Text Search]
H -->|No| J[Apply Sort]
I --> J
J --> K[Paginate Results]
K --> L[Return Tasks + Count]

style A fill:#e1f5ff
style L fill:#e1ffe1

πŸ”§ Business Logic & Functions​


pendingAction(options)​

Purpose: Retrieves all pending actions for an agency with rich contextual data. This is the primary dashboard view for agencies to see tasks requiring their attention, with comprehensive filtering, search, and sorting capabilities.

Parameters:

  • limit (Number) - Number of tasks to return per page (pagination)
  • skip (Number) - Number of tasks to skip (pagination offset)
  • search (String, optional) - Text search across title, type, account name, product name, user info
  • status (String, optional) - Filter by 'pending' or 'completed'
  • userId (ObjectId) - Current user ID (for audit/tracking)
  • parentAccount (ObjectId) - Agency account ID (data scope filter)
  • sortBy (String, optional) - Sort field: 'status', 'type', 'days', 'product_name', 'sent_on', 'created_at'
  • sortOrder (String, optional) - Sort direction: 'asc' | 'desc'

Returns: Promise<Object>

{
task_data: [
{
id: ObjectId,
title: String,
type: String,
status: String,
days: Number, // Days since created/sent
subaccount: { // Client account info
id, name, phone, email, image
},
order: { // Related order context
id, onboarding, assigned_user_ids, metadata, subscription
},
price: { // Service tier info
id, nickname, unit_amount, interval_count
},
responsible: [...], // Assigned team members
subscription: { // Active subscription
id, cancel_at, typeform
},
invoice: { // Open invoice if past_due
id, due_date
},
first_message: Boolean, // Needs agency response
has_reply: Boolean, // Has agency reply
last_message_by: String // Last message author
}
],
task_count: Number // Total matching tasks (for pagination)
}

Business Logic Flow:

  1. Base Filtering

    • Scopes to parent_account (data isolation)
    • Filters creator: 'dashclicks' (platform-created tasks only)
    • Excludes removed: true (soft-deleted tasks)
    • Excludes task types: ['setup', 'onboarding_qa', 'work_summary'] (internal-only tasks)
    • Optionally filters by status if provided
  2. Search Implementation

    When search term provided, performs case-insensitive regex match across:

    • title: Task summary
    • type: Task category
    • subaccount.name: Client business name
    • product.name: Service product name
    • responsible.name: Assigned user name
    • responsible.email: Assigned user email
    • responsible.phone: Assigned user phone

    Uses MongoDB $or array for multi-field matching.

  3. Rich Data Aggregation (MongoDB Pipeline)

    The function executes a complex 17-stage aggregation pipeline to join and enrich data:

    [
    // Stage 1: Match base filters
    { $match: { parent_account, creator: 'dashclicks', removed: { $ne: true }, ... } },

    // Stage 2-3: Lookup + unwind sub-account details
    { $lookup: { from: '_accounts', ... } },
    { $unwind: '$subaccount' },

    // Stage 4-5: Lookup + unwind order details
    { $lookup: { from: '_store.orders', ... } },
    { $unwind: '$order' },

    // Stage 6-7: Lookup + unwind price details
    { $lookup: { from: '_store.prices', ... } },
    { $unwind: '$price' },

    // Stage 8: Lookup open invoices (for past_due tasks)
    { $lookup: { from: '_store.invoices', ... } },

    // Stage 9: Lookup assigned users
    { $lookup: { from: '_users', ... } },

    // Stage 10-11: Lookup + unwind active subscription
    { $lookup: { from: '_store.subscriptions', pipeline: [
    { $match: { $expr: { $in: ['$status', ['active', 'past_due', 'trial']] } } }
    ]}},
    { $unwind: '$subscription' },

    // Stage 12: Lookup onboarding requests
    { $lookup: { from: 'onboarding.typeform.requests', ... } },

    // Stage 13: Apply search filter if provided
    ...(search ? [{ $match: searchMatch }] : []),

    // Stage 14-16: Calculate dynamic fields
    { $addFields: {
    sentDate: ..., // When task/form was sent
    approvedDate: ..., // When completed
    requests: { $first: '$requests' }
    }},
    { $addFields: { endDate: { $ifNull: ['$approvedDate', new Date()] } }},
    { $addFields: {
    days: { $dateDiff: { startDate: '$sentDate', endDate: '$endDate', unit: 'day' } }
    }},

    // Stage 17: Facet for data + count
    { $facet: {
    task_data: [
    { $lookup: { from: 'communications', ... } }, // Get replies
    { $sort: ... },
    { $skip: skip },
    { $limit: limit },
    { $project: ... } // Shape response
    ],
    task_count: [{ $count: 'count' }]
    }}
    ]
  4. Days Pending Calculation

    For each task, calculates how long it's been pending:

    • Onboarding tasks: Uses onboarding form sent date as start
    • Other tasks: Uses task created_at as start
    • End date: Uses approved date if completed, otherwise current date
    • Result: $dateDiff in days between start and end
  5. Communication Context

    Determines if task needs agency attention:

    • first_message: true - No outgoing agency responses yet
    • has_reply: true - Agency has replied at least once
    • last_message_by - 'platform' | 'agency' (who spoke last)

    Logic uses $filter on communications array:

    {
    $cond: {
    if: {
    $and: [
    { $in: ['$type', ['request', 'approval']] },
    { $ne: ['$status', 'completed'] }
    ]
    },
    then: {
    // Check if no OUTGOING (agency) messages exist
    first_message: { $eq: [{ $size: { $filter: OUTGOING }}, 0] },
    has_reply: { $gt: [{ $size: { $filter: OUTGOING }}, 0] }
    }
    }
    }
  6. Sorting

    Supports sorting by these mapped fields:

    • status β†’ $status
    • type β†’ $type
    • days β†’ $days (calculated)
    • product_name β†’ $metadata.product_name
    • sent_on β†’ $sentDate (calculated)
    • created_at β†’ $created_at

    Default sort: { created_at: -1 } (newest first)

  7. Pagination

    Uses MongoDB $skip and $limit operators in facet pipeline. Returns total count via separate count pipeline in same facet.

Key Business Rules:

  • βœ… Data Isolation: Always scoped to parent_account to prevent cross-agency data leakage
  • βœ… Active Subscriptions Only: Joins only filter subscriptions with status IN ['active', 'past_due', 'trial']
  • βœ… Creator Filter: Only shows creator: 'dashclicks' tasks (platform-originated), hides agency-created tasks
  • βœ… Type Exclusion: Hides internal task types (setup, onboarding_qa, work_summary) from dashboard
  • βœ… Soft Deletes: Respects removed: true flag for logical deletion

Error Handling:

  • Wrapped in try/catch with Promise.reject(error) propagation
  • Logs errors via logger.error() (not shown in return)
  • Returns empty arrays on aggregation failures (graceful degradation)

Performance Notes:

  • Heavy Aggregation: 17-stage pipeline with 6 lookupsβ€”requires robust indexes
  • Recommended Indexes:
    projects.tasks: { parent_account: 1, creator: 1, removed: 1, created_at: -1 }
    _accounts: { _id: 1 }
    _store.orders: { _id: 1, seller_account: 1 }
    _store.subscriptions: { _id: 1, status: 1 }
    _users: { _id: 1 }
    communications: { task_id: 1, type: 1, created_at: -1 }
  • Pagination Essential: Always use limit to avoid loading thousands of tasks

Example Usage:

const tasks = await pendingAction({
parentAccount: agencyAccountId,
userId: currentUserId,
status: 'pending',
search: 'facebook',
sortBy: 'days',
sortOrder: 'desc',
skip: 0,
limit: 20,
});

console.log(tasks.task_data.length); // 20 or fewer tasks
console.log(tasks.task_count[0].count); // Total matching tasks (e.g., 145)

Side Effects:

  • πŸ“– Read-only: Does not modify any data
  • ⚑ Performance Impact: Complex aggregation may take 100-500ms on large datasets
  • πŸ” Search Overhead: Regex searches add query time (consider text indexes for scale)

pendingActionCount(options)​

Purpose: Returns count-only summary of pending tasks by status. Used for badge counts and dashboard statistics without fetching full task data.

Parameters:

  • parentAccount (ObjectId) - Agency account ID

Returns: Promise<Object>

{
pending: Number, // Count of pending tasks
completed: Number, // Count of completed tasks
total: Number // Total task count
}

Business Logic:

  1. Uses same base filters as pendingAction
  2. Groups by status field using $group aggregation
  3. Returns counts for each status

Example Usage:

const counts = await pendingActionCount({ parentAccount: agencyId });
// { pending: 23, completed: 145, total: 168 }

myRequests(options)​

Purpose: Retrieves tasks created by the agency (not platform). Shows agency's own requests to platform, opposite of pendingAction.

Parameters:

  • userId (ObjectId) - Current user ID
  • parentAccount (ObjectId) - Agency account ID
  • limit (Number) - Pagination limit
  • skip (Number) - Pagination offset
  • search (String, optional) - Text search
  • status (String, optional) - Status filter
  • sortBy (String, optional) - Sort field
  • sortOrder (String, optional) - Sort direction

Returns: Promise<Object> (same structure as pendingAction)

Business Logic Differences from pendingAction:

  • Creator Filter: Matches creator !== 'dashclicks' (agency-created tasks)
  • Type Inclusion: Includes all task types (no exclusions)
  • Perspective: Shows tasks from agency's perspective as requester

Example Usage:

const myTasks = await myRequests({
parentAccount: agencyId,
userId: currentUserId,
status: 'pending',
limit: 20,
skip: 0,
});
// Returns tasks agency created, awaiting platform response

addTask(options)​

Purpose: Creates a new task with full validation, activity logging, and real-time notifications. Handles task creation from any party with appropriate context enrichment.

Parameters:

  • type (String, required) - Task type enum value
  • title (String, optional) - Task summary (auto-generated if not provided)
  • account_id (ObjectId, required) - Client account ID
  • parent_account (ObjectId, required) - Agency account ID
  • order_id (ObjectId, optional) - Related order ID
  • creator (String, required) - 'dashclicks' | user email
  • userId (ObjectId, required) - Creating user ID

Task Types:

const VALID_TYPES = [
'onboarding_issues', // Client onboarding has problems
'onboarding_submission', // Client submitted form
'onboarding_qa', // Platform QA review
'onboarding_approved', // Onboarding approved
'subscription_past_due', // Payment failed
'request', // General agency request
'approval', // Requires platform approval
'setup', // Service setup task
];

Returns: Promise<Object> (created task document)

Business Logic Flow:

  1. Validation

    • Validates type is in allowed enum
    • Validates account_id exists in _accounts
    • Validates order_id exists in _store.orders if provided
    • Throws errors on invalid data
  2. Title Auto-Generation

    If title not provided, generates based on type:

    const titleMap = {
    onboarding_issues: 'Onboarding Form Needs Attention',
    onboarding_submission: 'Client Submitted Onboarding Form',
    subscription_past_due: 'Subscription Payment Past Due',
    request: 'New Request from Agency',
    approval: 'Approval Required',
    setup: 'Service Setup Required',
    };
  3. Duplicate Prevention

    For certain types, checks if similar task already exists:

    if (type === 'subscription_past_due') {
    const existing = await ProjectsTask.findOne({
    account_id,
    parent_account,
    type: 'subscription_past_due',
    status: 'pending',
    });
    if (existing) throw badRequest('Past due task already exists');
    }
  4. Task Creation

    const task = await ProjectsTask.create({
    title,
    type,
    status: 'pending',
    account_id,
    parent_account,
    order_id,
    creator,
    created_at: new Date(),
    updated_at: new Date(),
    removed: false,
    });
  5. Activity Logging

    Logs task creation to activity stream:

    await createActivity({
    type: 'task_created',
    task_id: task._id,
    user_id: userId,
    account_id: parent_account,
    metadata: { task_type: type, task_title: title },
    });
  6. Real-time Notification

    Emits socket event to notify connected users:

    socketEmit('task_created', {
    parent_account,
    task: task.toJSON(),
    });

Error Handling:

  • 400 Bad Request: Invalid type, duplicate task, invalid account/order
  • 404 Not Found: Account or order doesn't exist
  • Propagates database errors

Side Effects:

  • ✏️ Writes: Creates task document
  • πŸ“ Activity Log: Creates activity record
  • πŸ”” Socket Event: Emits real-time notification
  • πŸ“§ Email (via queue): May trigger email notification based on task type

getTask(options)​

Purpose: Retrieves a single task with all contextual data and communication thread history. Used for task detail views.

Parameters:

  • id (ObjectId, required) - Task ID
  • userId (ObjectId, required) - Current user ID
  • parentAccount (ObjectId, required) - Agency account ID (authorization)
  • limit (Number, optional) - Communication thread limit (default: 20)
  • after (ObjectId, optional) - Cursor for pagination (communication after this ID)

Returns: Promise<Object>

{
id: ObjectId,
title: String,
type: String,
status: String,
account_id: ObjectId,
parent_account: ObjectId,
order_id: ObjectId,
creator: String,
created_at: Date,
updated_at: Date,
// Enriched data
subaccount: { ... }, // Client account details
order: { ... }, // Order context
subscription: { ... }, // Subscription status
communications: [ // Thread messages
{
id: ObjectId,
message: String,
type: 'OUTGOING' | 'INCOMING',
attachments: Array,
user: { id, name, email, image },
created_at: Date
}
]
}

Business Logic:

  1. Authorization Check

    Verifies task belongs to parentAccount:

    const task = await ProjectsTask.findOne({
    _id: id,
    parent_account: parentAccount,
    });
    if (!task) throw notFound('Task not found');
  2. Enrichment Aggregation

    Similar to pendingAction but for single task:

    • Lookups account, order, subscription, users
    • Joins communication thread
  3. Communication Thread Pagination

    Supports cursor-based pagination:

    {
    $lookup: {
    from: 'communications',
    pipeline: [
    { $match: { task_id: id, ...(after ? { _id: { $gt: after } } : {}) } },
    { $sort: { created_at: 1 } },
    { $limit: limit }
    ]
    }
    }

Example Usage:

const task = await getTask({
id: taskId,
userId: currentUserId,
parentAccount: agencyId,
limit: 50,
});

reply(options)​

Purpose: Adds a reply message to a task's communication thread. Updates task timestamp and sends notifications.

Parameters:

  • id (ObjectId, required) - Task ID
  • reply (String, required) - Message content
  • attachments (Array, optional) - File attachment references
  • userId (ObjectId, required) - Replying user ID
  • parentAccount (ObjectId, required) - Agency account ID

Returns: Promise<Object> (created communication document)

Business Logic Flow:

  1. Task Validation

    const task = await ProjectsTask.findOne({
    _id: id,
    parent_account: parentAccount,
    });
    if (!task) throw notFound('Task not found');
  2. Determine Message Direction

    const messageType = task.creator === 'dashclicks' ? 'OUTGOING' : 'INCOMING';
    // If platform created task, agency reply is OUTGOING
    // If agency created task, agency reply is INCOMING (to platform)
  3. Create Communication

    const communication = await Communications.create({
    task_id: id,
    message: reply,
    type: messageType,
    attachments: attachments || [],
    user_id: userId,
    created_at: new Date(),
    });
  4. Update Task Timestamp

    await ProjectsTask.updateOne({ _id: id }, { updated_at: new Date() });
  5. Activity Logging

    await createActivity({
    type: 'task_reply',
    task_id: id,
    user_id: userId,
    account_id: parentAccount,
    metadata: { message_preview: reply.substring(0, 100) },
    });
  6. Real-time Notification

    socketEmit('task_reply', {
    parent_account: parentAccount,
    task_id: id,
    communication: communication.toJSON(),
    });

Side Effects:

  • ✏️ Writes: Creates communication document, updates task timestamp
  • πŸ“ Activity Log: Logs reply action
  • πŸ”” Socket Event: Notifies connected users
  • πŸ“§ Email: May trigger email notification to task creator

updateTask(options)​

Purpose: Updates task status or metadata. Primary use case: marking tasks as completed.

Parameters:

  • id (ObjectId, required) - Task ID
  • status (String, optional) - New status ('pending' | 'completed')
  • title (String, optional) - Updated title
  • userId (ObjectId, required) - Updating user ID
  • parentAccount (ObjectId, required) - Agency account ID

Returns: Promise<Object> (updated task document)

Business Logic:

  1. Authorization Check

  2. Status Change Logic

    If changing to completed:

    if (status === 'completed') {
    // Log completion activity
    await createActivity({
    type: 'task_completed',
    task_id: id,
    user_id: userId,
    account_id: parentAccount,
    });

    // Emit socket event
    socketEmit('task_completed', { parent_account: parentAccount, task_id: id });
    }
  3. Update Task

    const updatedTask = await ProjectsTask.findOneAndUpdate(
    { _id: id, parent_account: parentAccount },
    { status, title, updated_at: new Date() },
    { new: true }, // Return updated document
    );

Side Effects:

  • ✏️ Writes: Updates task document
  • πŸ“ Activity Log: Logs status change
  • πŸ”” Socket Event: Notifies of completion

deleteTask(options)​

Purpose: Soft deletes a task by setting removed: true. Does not physically delete for audit trail preservation.

Parameters:

  • id (ObjectId, required) - Task ID
  • userId (ObjectId, required) - Deleting user ID
  • parentAccount (ObjectId, required) - Agency account ID

Returns: Promise<Object> (success confirmation)

Business Logic:

  1. Authorization Check

  2. Soft Delete

    await ProjectsTask.updateOne(
    { _id: id, parent_account: parentAccount },
    { removed: true, updated_at: new Date() },
    );
  3. Activity Logging

    await createActivity({
    type: 'task_deleted',
    task_id: id,
    user_id: userId,
    account_id: parentAccount,
    });
  4. Socket Notification

    socketEmit('task_deleted', { parent_account: parentAccount, task_id: id });

Key Business Rule:

  • ⚠️ Soft Delete Only: Task remains in database with removed: true for audit compliance
  • ⚠️ Communication Preservation: Associated communications are NOT deleted
  • ⚠️ Activity Trail: Delete action is logged to activity stream

Side Effects:

  • ✏️ Writes: Sets removed: true flag
  • πŸ“ Activity Log: Logs deletion
  • πŸ”” Socket Event: Notifies of deletion

πŸ”€ Integration Points​

Internal Dependencies​

  • createActivity() (utilities/project.js) - Activity stream logging
  • socketEmit() (utilities/index.js) - Real-time WebSocket notifications
  • Store Module - Order and subscription context
  • Accounts Module - Account validation and details
  • Users Module - User assignment and details

External Services​

  • Socket.IO (general-socket service) - Real-time push notifications
  • Email Queue - Notification emails for task events

πŸ§ͺ Edge Cases & Special Handling​

Case: Orphaned Tasks​

Condition: Task references deleted order or inactive subscription

Handling:

  • Aggregation uses preserveNullAndEmptyArrays: true in $unwind
  • Tasks remain visible with null order/subscription
  • UI should handle null references gracefully

Case: Past-Due Duplicate Prevention​

Condition: Multiple payment failures for same subscription

Handling:

if (type === 'subscription_past_due') {
const existing = await ProjectsTask.findOne({
account_id,
parent_account,
type: 'subscription_past_due',
status: 'pending',
});
if (existing) throw badRequest('Past due task already exists for this account');
}

Only one active past-due task per client at a time.

Case: Communication Direction Logic​

Condition: Determining if message is OUTGOING or INCOMING

Handling:

const messageType = task.creator === 'dashclicks' ? 'OUTGOING' : 'INCOMING';
  • Platform-created task: Agency replies are OUTGOING (agency β†’ platform)
  • Agency-created task: Agency replies are INCOMING (agency β†’ platform)

This enables bidirectional communication with proper direction tracking.


⚠️ Important Notes​

  • πŸ”’ Data Isolation: All queries MUST filter by parent_account to prevent cross-agency data access
  • ⚑ Performance: The pendingAction aggregation is expensiveβ€”always use pagination and consider caching task counts
  • πŸ“Š Real-time Updates: Socket events are criticalβ€”ensure general-socket service is healthy
  • πŸ—‘οΈ Soft Deletes: Never hard-delete tasks; use removed: true for compliance
  • πŸ“§ Email Rate Limiting: Task creation may trigger email notificationsβ€”consider rate limits for bulk operations
  • πŸ”„ Eventual Consistency: Socket events are fire-and-forget; clients should refresh on reconnect

πŸ’¬

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