40094-vm/backend/src/services/salesWorkflow.js
2026-05-26 15:03:21 +00:00

115 lines
3.0 KiB
JavaScript

const db = require('../db/models');
const ActivitiesDBApi = require('../db/api/activities');
const ACTIVITY_TYPES = new Set(['call', 'email', 'meeting', 'task', 'demo', 'note']);
function httpError(message, code = 400) {
const error = new Error(message);
error.code = code;
return error;
}
function cleanText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function parseRequiredDate(value, fieldName) {
const date = new Date(value);
if (!value || Number.isNaN(date.getTime())) {
throw httpError(`${fieldName} must be a valid date and time.`);
}
return date;
}
module.exports = class SalesWorkflowService {
static async scheduleFollowUp(data, currentUser) {
if (!currentUser || !currentUser.id) {
throw httpError('You must be signed in to schedule a follow-up.', 403);
}
const dealId = cleanText(data?.dealId || data?.deal);
const activityType = cleanText(data?.activity_type) || 'task';
const subject = cleanText(data?.subject);
const details = cleanText(data?.details);
const dueAt = parseRequiredDate(data?.due_at, 'Follow-up time');
const reminderAt = data?.reminder_at
? parseRequiredDate(data.reminder_at, 'Reminder time')
: new Date(dueAt.getTime() - 30 * 60 * 1000);
if (!dealId) {
throw httpError('A deal is required before scheduling a follow-up.');
}
if (!ACTIVITY_TYPES.has(activityType)) {
throw httpError('Activity type must be call, email, meeting, task, demo, or note.');
}
if (!subject) {
throw httpError('Subject is required before scheduling a follow-up.');
}
const transaction = await db.sequelize.transaction();
try {
const deal = await db.deals.findByPk(dealId, { transaction });
if (!deal) {
throw httpError('Deal not found.', 404);
}
const activityPayload = {
activity_type: activityType,
subject,
details,
scheduled_at: dueAt,
due_at: dueAt,
status: 'planned',
is_reminder_enabled: data?.is_reminder_enabled !== false,
reminder_at: reminderAt,
assigned_to: currentUser.id,
deal: deal.id,
lead: deal.leadId || null,
contact: deal.primary_contactId || null,
};
const activity = await ActivitiesDBApi.create(
activityPayload,
{
currentUser,
transaction,
},
);
await deal.update(
{
next_follow_up_at: dueAt,
updatedById: currentUser.id,
},
{ transaction },
);
await transaction.commit();
return {
activity: {
id: activity.id,
activity_type: activity.activity_type,
subject: activity.subject,
due_at: activity.due_at,
status: activity.status,
},
deal: {
id: deal.id,
name: deal.name,
next_follow_up_at: deal.next_follow_up_at,
},
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
};