Templates Controller
๐ Overviewโ
Controller Path: internal/api/v1/templates/Controllers/template.js
The Templates controller provides template management for various communication and workflow types with support for both system-provided (default) and user-created templates. Core responsibilities include:
- Template CRUD: Create, read, update, and delete user templates
- System Templates: Distribute default templates to specific main accounts
- Multi-Source: Combine system and user templates in single response
- Type Filtering: Email, SMS, proposals, tasks, etc.
- Category Filtering: Organize templates by business categories
- Search: Full-text search across title and body
- Analytics: Track template creation trends by date segments
๐๏ธ Collections Usedโ
- templates (link removed - file does not exist) - User-created templates
- default-templates (link removed - file does not exist) - System-provided templates
- main-account-default-templates (link removed - file does not exist) - Template distribution to accounts
๐ Data Flowโ
Template Listing with Multiple Sourcesโ
flowchart TD
A[Get Templates Request] --> B{Source Type?}
B -->|system| C[Query default-templates]
B -->|user| D[Query templates]
B -->|all| E[Query both collections]
C --> F[Check main account permissions]
F --> G{Is main account?}
G -->|Yes| H[Include system templates]
G -->|No| I[Empty array]
D --> J[Filter by account/user]
E --> K[Merge system + user templates]
H --> L[Apply filters]
I --> L
J --> L
K --> L
L --> M{Has type filter?}
M -->|Yes| N[Filter by type]
M -->|No| O[All types]
N --> P{Has category filter?}
P -->|Yes| Q[Filter by category]
P -->|No| R[All categories]
O --> P
Q --> S{Has search query?}
R --> S
S -->|Yes| T[Search title + body]
S -->|No| U[No search]
T --> V[Apply pagination]
U --> V
V --> W[Calculate category counts]
W --> X[Calculate type counts]
X --> Y[Return paginated results]
style C fill:#e3f2fd
style D fill:#e3f2fd
style E fill:#fff4e6
style W fill:#e8f5e9
style X fill:#e8f5e9
Template Creation Flowโ
sequenceDiagram
participant User
participant Controller
participant TemplateModel
participant ValidationMiddleware
User->>Controller: POST /templates
Note over User,Controller: { type, category, title, body }
Controller->>ValidationMiddleware: Validate request body
ValidationMiddleware-->>Controller: Validation result
Controller->>TemplateModel: Create new template
Note over Controller,TemplateModel: account_id + user_id added
TemplateModel->>TemplateModel: Save to database
TemplateModel-->>Controller: Saved template
Controller->>Controller: Transform response
Controller-->>User: Created template
style Controller fill:#e3f2fd
style TemplateModel fill:#fff4e6
๐ง Business Logic & Functionsโ
getTemplates({ src, type, category, page, limit, search, sortOrder, sortField })โ
Purpose: Get templates from multiple sources (system, user, or both) with filtering and pagination
Parameters:
src(String) - Source: 'system', 'user', or 'all' (default: 'user')type(String) - Template type filter (email, sms, proposal, task, etc.)category(String) - Category filterpage(Number) - Page number (default: 1)limit(Number) - Results per page (default: 10)search(String) - Search query for title/bodysortOrder(String) - 'asc' or 'desc' (default: 'desc')sortField(String) - Field to sort by (default: 'createdAt')
Context:
this.request- Express request objectthis.request.headers['x-account-id']- Account IDthis.request.user._id- User IDthis.request.user.main_account- Main account ID (for system templates)
Returns:
{
success: true,
message: 'SUCCESS',
data: {
templates: [
{
id: String,
title: String,
body: String,
type: String, // 'email', 'sms', 'proposal', 'task', etc.
category: String,
account_id: ObjectId, // null for system templates
user_id: ObjectId, // null for system templates
createdAt: Date,
updatedAt: Date
}
],
total: Number, // Total matching templates
category_count: { // Count per category
'Business Development': Number,
'Sales': Number,
...
},
type_count: { // Count per type
'email': Number,
'sms': Number,
...
}
}
}
Business Logic Flow:
-
Determine Source
const src = this.request.query.src || 'user';
let templates = [];
if (src === 'system') {
// Get system templates only
const main_account_id = this.request.user.main_account;
const default_template_ids = await MainAccountDefaultTemplate.distinct('default_template_id', {
main_account_id,
});
templates = await DefaultTemplate.find({
_id: { $in: default_template_ids },
});
} else if (src === 'user') {
// Get user templates only
templates = await Template.find({
account_id: this.request.headers['x-account-id'],
});
} else {
// Get both system and user templates
const main_account_id = this.request.user.main_account;
const default_template_ids = await MainAccountDefaultTemplate.distinct('default_template_id', {
main_account_id,
});
const [systemTemplates, userTemplates] = await Promise.all([
DefaultTemplate.find({ _id: { $in: default_template_ids } }),
Template.find({ account_id: this.request.headers['x-account-id'] }),
]);
templates = [...systemTemplates, ...userTemplates];
} -
Apply Type Filter
if (type) {
templates = templates.filter(template => template.type === type);
} -
Apply Category Filter
if (category) {
templates = templates.filter(template => template.category === category);
} -
Apply Search
if (search) {
const searchRegex = new RegExp(search, 'i');
templates = templates.filter(
template => searchRegex.test(template.title) || searchRegex.test(template.body),
);
} -
Calculate Counts Before Pagination
const category_count = {};
const type_count = {};
templates.forEach(template => {
// Category counts
if (template.category) {
category_count[template.category] = (category_count[template.category] || 0) + 1;
}
// Type counts
if (template.type) {
type_count[template.type] = (type_count[template.type] || 0) + 1;
}
}); -
Apply Sorting
const sortOrder = this.request.query.sortOrder === 'asc' ? 1 : -1;
const sortField = this.request.query.sortField || 'createdAt';
templates.sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
if (aVal < bVal) return -1 * sortOrder;
if (aVal > bVal) return 1 * sortOrder;
return 0;
}); -
Apply Pagination
const page = parseInt(this.request.query.page) || 1;
const limit = parseInt(this.request.query.limit) || 10;
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedTemplates = templates.slice(startIndex, endIndex); -
Return Response
return {
success: true,
message: 'SUCCESS',
data: {
templates: paginatedTemplates.map(t => ({
id: t._id?.toString(),
title: t.title,
body: t.body,
type: t.type,
category: t.category,
account_id: t.account_id,
user_id: t.user_id,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
})),
total: templates.length,
category_count,
type_count,
},
};
Key Business Rules:
- System Templates: Only available to accounts with main_account_id
- Source Merging: 'all' combines system + user templates in single list
- Counts Before Pagination: Category and type counts reflect all matching templates, not just current page
- Case-Insensitive Search: Searches both title and body fields
- Default Sorting: Most recent templates first (createdAt desc)
get({ type, search, page, limit, sortOrder, sortField })โ
Purpose: Get user templates only (no system templates) with filtering and pagination
Parameters:
type(String) - Template type filtersearch(String) - Search querypage(Number) - Page number (default: 1)limit(Number) - Results per page (default: 10)sortOrder(String) - 'asc' or 'desc' (default: 'desc')sortField(String) - Sort field (default: 'createdAt')
Context:
this.request.headers['x-account-id']- Account ID
Returns:
{
success: true,
message: 'SUCCESS',
data: {
templates: Array,
total: Number
}
}
Business Logic Flow: Similar to getTemplates() but always uses 'user' source
getOne({ templateID })โ
Purpose: Get a single template by ID with ownership validation
Parameters:
templateID(String) - Template ID
Context:
this.request.headers['x-account-id']- Account ID
Returns:
{
success: true,
message: 'SUCCESS',
data: {
id: String,
title: String,
body: String,
type: String,
category: String,
account_id: ObjectId,
user_id: ObjectId,
createdAt: Date,
updatedAt: Date
}
}
Business Logic Flow:
-
Find Template
const template = await Template.findOne({
_id: templateID,
account_id: this.request.headers['x-account-id'],
});
if (!template) {
throw new Error('Template not found');
} -
Return Template
return {
success: true,
message: 'SUCCESS',
data: {
id: template._id.toString(),
title: template.title,
body: template.body,
type: template.type,
category: template.category,
account_id: template.account_id,
user_id: template.user_id,
createdAt: template.createdAt,
updatedAt: template.updatedAt,
},
};
Key Business Rules:
- Ownership Validation: Template must belong to requesting account
- Error Handling: Returns error if template not found or belongs to different account
post({ type, body })โ
Purpose: Create a new template for the current account and user
Parameters:
type(String) - Template type (email, sms, proposal, task, etc.)body(Object) - Template data:title(String) - Template titlebody(String) - Template contentcategory(String) - Template category
Context:
this.request.headers['x-account-id']- Account IDthis.request.user._id- User ID
Returns:
{
success: true,
message: 'SUCCESS',
data: {
id: String,
title: String,
body: String,
type: String,
category: String,
account_id: ObjectId,
user_id: ObjectId,
createdAt: Date,
updatedAt: Date
}
}
Business Logic Flow:
-
Create Template
const newTemplate = await Template.create({
type: type,
title: body.title,
body: body.body,
category: body.category,
account_id: this.request.headers['x-account-id'],
user_id: this.request.user._id,
}); -
Return Created Template
return {
success: true,
message: 'SUCCESS',
data: {
id: newTemplate._id.toString(),
title: newTemplate.title,
body: newTemplate.body,
type: newTemplate.type,
category: newTemplate.category,
account_id: newTemplate.account_id,
user_id: newTemplate.user_id,
createdAt: newTemplate.createdAt,
updatedAt: newTemplate.updatedAt,
},
};
Key Business Rules:
- Auto-Assignment: Account ID and user ID automatically assigned from request context
- Validation: Middleware validates required fields (title, body, type)
put({ templateID, body })โ
Purpose: Update an existing template with ownership validation
Parameters:
templateID(String) - Template ID to updatebody(Object) - Updated template data:title(String) - Updated titlebody(String) - Updated contentcategory(String) - Updated category
Context:
this.request.headers['x-account-id']- Account ID
Returns:
{
success: true,
message: 'SUCCESS',
data: {
id: String,
title: String,
body: String,
type: String,
category: String,
account_id: ObjectId,
user_id: ObjectId,
createdAt: Date,
updatedAt: Date
}
}
Business Logic Flow:
-
Find and Validate Ownership
const template = await Template.findOne({
_id: templateID,
account_id: this.request.headers['x-account-id'],
});
if (!template) {
throw new Error('Template not found');
} -
Update Template
template.title = body.title || template.title;
template.body = body.body || template.body;
template.category = body.category || template.category;
await template.save(); -
Return Updated Template
return {
success: true,
message: 'SUCCESS',
data: {
id: template._id.toString(),
title: template.title,
body: template.body,
type: template.type,
category: template.category,
account_id: template.account_id,
user_id: template.user_id,
createdAt: template.createdAt,
updatedAt: template.updatedAt,
},
};
Key Business Rules:
- Ownership Validation: Only templates belonging to requesting account can be updated
- Partial Updates: Omitted fields retain existing values
- System Templates: Cannot be updated (not in templates collection)
delete({ templateID })โ
Purpose: Delete a template with ownership validation
Parameters:
templateID(String) - Template ID to delete
Context:
this.request.headers['x-account-id']- Account ID
Returns:
{
success: true,
message: 'SUCCESS'
}
Business Logic Flow:
-
Find and Validate Ownership
const template = await Template.findOne({
_id: templateID,
account_id: this.request.headers['x-account-id'],
});
if (!template) {
throw new Error('Template not found');
} -
Delete Template
await Template.deleteOne({ _id: templateID });
return {
success: true,
message: 'SUCCESS',
};
Key Business Rules:
- Ownership Validation: Only templates belonging to requesting account can be deleted
- System Templates: Cannot be deleted (not in templates collection)
getTotalNew({ dateSegment, startDate, endDate })โ
Purpose: Get template count by predefined date segments or custom date range
Parameters:
dateSegment(String) - Predefined segment: 'this_week', 'last_week', 'this_month', 'last_six_month', 'this_year'startDate(Date) - Custom start date (if no dateSegment)endDate(Date) - Custom end date (if no dateSegment)
Context:
this.request.headers['x-account-id']- Account ID
Returns:
{
success: true,
message: 'SUCCESS',
data: {
total: Number
}
}
Business Logic Flow:
-
Calculate Date Range
const moment = require('moment');
let startDate, endDate;
switch (dateSegment) {
case 'this_week':
startDate = moment().startOf('week').toDate();
endDate = moment().endOf('week').toDate();
break;
case 'last_week':
startDate = moment().subtract(1, 'week').startOf('week').toDate();
endDate = moment().subtract(1, 'week').endOf('week').toDate();
break;
case 'this_month':
startDate = moment().startOf('month').toDate();
endDate = moment().endOf('month').toDate();
break;
case 'last_six_month':
startDate = moment().subtract(6, 'month').startOf('month').toDate();
endDate = moment().endOf('month').toDate();
break;
case 'this_year':
startDate = moment().startOf('year').toDate();
endDate = moment().endOf('year').toDate();
break;
default:
// Use custom dates if provided
startDate = this.request.query.startDate;
endDate = this.request.query.endDate;
} -
Count Templates in Date Range
const count = await Template.countDocuments({
account_id: this.request.headers['x-account-id'],
createdAt: {
$gte: startDate,
$lte: endDate,
},
}); -
Return Count
return {
success: true,
message: 'SUCCESS',
data: {
total: count,
},
};
Key Business Rules:
- Predefined Segments: Standard business time periods for analytics
- Custom Range: Supports arbitrary date ranges via startDate/endDate
- Account Scoped: Only counts templates for requesting account
๐ Integration Pointsโ
Email/SMS Campaignsโ
Load Template:
// Get email templates
const response = await templateController.getTemplates({
src: 'all',
type: 'email',
category: 'Sales',
});
// Apply template to campaign
const campaignContent = response.data.templates[0].body;
Proposal Generationโ
Use Proposal Template:
// Get proposal templates
const response = await templateController.getTemplates({
src: 'system',
type: 'proposal',
});
// Apply template with dynamic data
const proposal = response.data.templates[0].body
.replace('{{client_name}}', clientName)
.replace('{{amount}}', amount);
Task Automationโ
Create Task from Template:
// Get task templates
const response = await templateController.getTemplates({
src: 'user',
type: 'task',
});
// Create task with template content
const task = {
title: response.data.templates[0].title,
description: response.data.templates[0].body,
};
๐งช Edge Cases & Special Handlingโ
System Templates Only for Main Accountsโ
Non-Main Accounts Get Empty System Templates:
if (src === 'system') {
const main_account_id = this.request.user.main_account;
if (!main_account_id) {
// Non-main accounts have no system templates
return {
success: true,
message: 'SUCCESS',
data: {
templates: [],
total: 0,
category_count: {},
type_count: {},
},
};
}
}
Empty Search Resultsโ
No Matching Templates:
if (templates.length === 0) {
return {
success: true,
message: 'SUCCESS',
data: {
templates: [],
total: 0,
category_count: {},
type_count: {},
},
};
}
Invalid Date Segmentโ
Custom Dates Required:
if (!dateSegment && (!startDate || !endDate)) {
throw new Error('Either dateSegment or startDate/endDate required');
}
Template Not Foundโ
Ownership Validation Failure:
const template = await Template.findOne({
_id: templateID,
account_id: accountID,
});
if (!template) {
throw new Error('Template not found or access denied');
}
โ ๏ธ Important Notesโ
-
System Templates: Only available to main accounts with assigned default templates via
main-account-default-templatescollection. -
Source Merging: Using
src='all'combines system and user templates in a single list, useful for template pickers. -
Counts: Category and type counts reflect all matching templates, not just the current page.
-
In-Memory Operations: Filtering, search, and sorting happen in-memory after fetching templates. For large template sets, consider database-level operations.
-
Ownership: All CRUD operations validate account ownership. Users cannot access templates from other accounts.
-
System Template Immutability: System templates cannot be edited or deleted through user endpoints.
-
Search: Case-insensitive search across both title and body fields using regex.
-
Date Segments: Predefined segments use moment.js for consistent date calculations (week starts Monday).
-
Pagination: Applied after filtering/search, so counts are accurate for filtered results.
-
Type Safety: Template types are not enforced by enum, allowing flexible template types.
-
Category Management: Categories are free-form strings, allowing custom categorization.
-
Main Account Distribution: System templates distributed to specific main accounts via junction table.
๐ Related Documentationโ
- Email Campaigns (link removed - file does not exist) - Email template usage
- SMS Campaigns (link removed - file does not exist) - SMS template usage
- Proposals Module (link removed - file does not exist) - Proposal template integration
- Tasks Module (link removed - file does not exist) - Task template usage
- Main Accounts (link removed - file does not exist) - System template distribution