edtech / apps /api /src /services /crm-service.ts
CognxSafeTrack
fix(security): resolve critical and high technical debt from audit
a8e18d6
import { prisma } from './prisma';
import { whatsappQueue } from './queue';
import { ContactService } from './ContactService';
export class CRMService {
static async importFromXlsx(organizationId: string, fileBuffer: Buffer, listName: string | null) {
const XLSX = await import('xlsx');
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
const finalListName = listName || `Liste du ${new Date().toLocaleDateString('fr-FR')}`;
const broadcastList = await prisma.broadcastList.create({
data: { name: finalListName, organizationId }
});
const results = { created: 0, updated: 0, errors: 0 };
for (const row of rows as Record<string, unknown>[]) {
try {
const batch = await ContactService.bulkImport(organizationId, [row], broadcastList.id);
results.created += batch.created;
results.errors += batch.errors;
} catch (err) {
results.errors++;
}
}
return { listId: broadcastList.id, listName: finalListName, results };
}
static async listContacts(organizationId: string, filterTags?: string[]) {
return prisma.contact.findMany({
where: {
organizationId,
...(filterTags && filterTags.length > 0
? { tags: { hasSome: filterTags } }
: {}),
},
orderBy: { createdAt: 'desc' },
take: 1000,
});
}
static async deleteContact(organizationId: string, contactId: string) {
return prisma.contact.delete({ where: { id: contactId, organizationId } });
}
static async listMessages(organizationId: string, limit: number = 50, skip: number = 0) {
const [messages, total] = await Promise.all([
prisma.message.findMany({
where: { organizationId },
include: { contact: true },
orderBy: { createdAt: 'desc' },
take: Math.min(limit, 100),
skip,
}),
prisma.message.count({ where: { organizationId } }),
]);
return { data: messages, total, limit, skip };
}
static async replyToContact(organizationId: string, contactId: string, content: string) {
const newMessage = await prisma.message.create({
data: {
organizationId,
contactId,
content,
direction: 'OUTBOUND',
channel: 'WHATSAPP'
},
include: { contact: true }
});
await whatsappQueue.add('send-direct-message', {
messageId: newMessage.id,
contactId,
content,
organizationId
});
return newMessage;
}
static async getCampaignHistory(organizationId: string, page: number, limit: number, status?: string) {
const skip = (page - 1) * limit;
const VALID_STATUSES = ['SENT', 'DELIVERED', 'READ', 'FAILED'] as const;
type CampaignStatus = typeof VALID_STATUSES[number];
const validStatus = status && (VALID_STATUSES as readonly string[]).includes(status)
? (status as CampaignStatus)
: undefined;
const where = { organizationId, ...(validStatus ? { status: validStatus } : {}) };
const [records, total, statusCounts] = await Promise.all([
prisma.campaignHistory.findMany({
where,
orderBy: { sentAt: 'desc' },
skip,
take: limit,
include: { contact: { select: { name: true, phoneNumber: true } } }
}),
prisma.campaignHistory.count({ where }),
prisma.campaignHistory.groupBy({
by: ['status'],
where: { organizationId },
_count: { _all: true }
})
]);
const stats = { SENT: 0, DELIVERED: 0, READ: 0, FAILED: 0 } as Record<string, number>;
for (const row of statusCounts) stats[row.status] = row._count._all;
return { records, total, page, limit, stats };
}
static async bulkImportJson(organizationId: string, contacts: any[], listName?: string) {
const finalListName = listName || `Import du ${new Date().toLocaleDateString('fr-FR')}`;
const broadcastList = await prisma.broadcastList.create({
data: { name: finalListName, organizationId }
});
const results = await ContactService.bulkImport(organizationId, contacts, broadcastList.id);
return { listId: broadcastList.id, listName: finalListName, results };
}
}