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 infostatus(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:
-
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
statusif provided
- Scopes to
-
Search Implementation
When
searchterm provided, performs case-insensitive regex match across:title: Task summarytype: Task categorysubaccount.name: Client business nameproduct.name: Service product nameresponsible.name: Assigned user nameresponsible.email: Assigned user emailresponsible.phone: Assigned user phone
Uses MongoDB
$orarray for multi-field matching. -
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' }]
}}
] -
Days Pending Calculation
For each task, calculates how long it's been pending:
- Onboarding tasks: Uses onboarding form
sentdate as start - Other tasks: Uses task
created_atas start - End date: Uses
approveddate if completed, otherwise current date - Result:
$dateDiffin days between start and end
- Onboarding tasks: Uses onboarding form
-
Communication Context
Determines if task needs agency attention:
first_message: true- No outgoing agency responses yethas_reply: true- Agency has replied at least oncelast_message_by- 'platform' | 'agency' (who spoke last)
Logic uses
$filteron 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] }
}
}
} -
Sorting
Supports sorting by these mapped fields:
statusβ$statustypeβ$typedaysβ$days(calculated)product_nameβ$metadata.product_namesent_onβ$sentDate(calculated)created_atβ$created_at
Default sort:
{ created_at: -1 }(newest first) -
Pagination
Uses MongoDB
$skipand$limitoperators in facet pipeline. Returns total count via separate count pipeline in same facet.
Key Business Rules:
- β
Data Isolation: Always scoped to
parent_accountto 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: trueflag 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
limitto 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:
- Uses same base filters as
pendingAction - Groups by
statusfield using$groupaggregation - 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 IDparentAccount(ObjectId) - Agency account IDlimit(Number) - Pagination limitskip(Number) - Pagination offsetsearch(String, optional) - Text searchstatus(String, optional) - Status filtersortBy(String, optional) - Sort fieldsortOrder(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 valuetitle(String, optional) - Task summary (auto-generated if not provided)account_id(ObjectId, required) - Client account IDparent_account(ObjectId, required) - Agency account IDorder_id(ObjectId, optional) - Related order IDcreator(String, required) - 'dashclicks' | user emailuserId(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:
-
Validation
- Validates
typeis in allowed enum - Validates
account_idexists in_accounts - Validates
order_idexists in_store.ordersif provided - Throws errors on invalid data
- Validates
-
Title Auto-Generation
If
titlenot provided, generates based ontype: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',
}; -
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');
} -
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,
}); -
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 },
}); -
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/order404 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 IDuserId(ObjectId, required) - Current user IDparentAccount(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:
-
Authorization Check
Verifies task belongs to
parentAccount:const task = await ProjectsTask.findOne({
_id: id,
parent_account: parentAccount,
});
if (!task) throw notFound('Task not found'); -
Enrichment Aggregation
Similar to
pendingActionbut for single task:- Lookups account, order, subscription, users
- Joins communication thread
-
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 IDreply(String, required) - Message contentattachments(Array, optional) - File attachment referencesuserId(ObjectId, required) - Replying user IDparentAccount(ObjectId, required) - Agency account ID
Returns: Promise<Object> (created communication document)
Business Logic Flow:
-
Task Validation
const task = await ProjectsTask.findOne({
_id: id,
parent_account: parentAccount,
});
if (!task) throw notFound('Task not found'); -
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) -
Create Communication
const communication = await Communications.create({
task_id: id,
message: reply,
type: messageType,
attachments: attachments || [],
user_id: userId,
created_at: new Date(),
}); -
Update Task Timestamp
await ProjectsTask.updateOne({ _id: id }, { updated_at: new Date() }); -
Activity Logging
await createActivity({
type: 'task_reply',
task_id: id,
user_id: userId,
account_id: parentAccount,
metadata: { message_preview: reply.substring(0, 100) },
}); -
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 IDstatus(String, optional) - New status ('pending' | 'completed')title(String, optional) - Updated titleuserId(ObjectId, required) - Updating user IDparentAccount(ObjectId, required) - Agency account ID
Returns: Promise<Object> (updated task document)
Business Logic:
-
Authorization Check
-
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 });
} -
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 IDuserId(ObjectId, required) - Deleting user IDparentAccount(ObjectId, required) - Agency account ID
Returns: Promise<Object> (success confirmation)
Business Logic:
-
Authorization Check
-
Soft Delete
await ProjectsTask.updateOne(
{ _id: id, parent_account: parentAccount },
{ removed: true, updated_at: new Date() },
); -
Activity Logging
await createActivity({
type: 'task_deleted',
task_id: id,
user_id: userId,
account_id: parentAccount,
}); -
Socket Notification
socketEmit('task_deleted', { parent_account: parentAccount, task_id: id });
Key Business Rule:
- β οΈ Soft Delete Only: Task remains in database with
removed: truefor audit compliance - β οΈ Communication Preservation: Associated communications are NOT deleted
- β οΈ Activity Trail: Delete action is logged to activity stream
Side Effects:
- βοΈ Writes: Sets
removed: trueflag - π Activity Log: Logs deletion
- π Socket Event: Notifies of deletion
π Integration Pointsβ
Internal Dependenciesβ
createActivity()(utilities/project.js) - Activity stream loggingsocketEmit()(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: truein$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_accountto prevent cross-agency data access - β‘ Performance: The
pendingActionaggregation 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: truefor 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
π Related Documentationβ
- Onboarding Management - Related onboarding workflow
- Activity Tracking - Activity logging system
- Projects Module Overview - Module architecture