edtech / apps /api /src /services /ContactService.ts
CognxSafeTrack
feat(crm): optimize multimedia pipeline and harden contact schema
c5fde49
import { prisma } from './prisma';
import { logger } from '../logger';
export interface ContactImportRow {
[key: string]: any;
}
export interface BulkImportResult {
created: number;
updated: number;
errors: number;
}
export class ContactService {
/**
* Identifies the phone number column using fuzzy matching
*/
static findPhoneKey(row: ContactImportRow): string | undefined {
return Object.keys(row).find(k =>
k.toLowerCase().includes('phone') ||
k.toLowerCase().includes('téléphone') ||
k.toLowerCase().includes('tel') ||
k.toLowerCase().includes('num') ||
k.toLowerCase().includes('whatsapp')
);
}
/**
* Identifies the name column using fuzzy matching
*/
static findNameKey(row: ContactImportRow): string | undefined {
return Object.keys(row).find(k =>
k.toLowerCase().includes('name') ||
k.toLowerCase().includes('nom') ||
k.toLowerCase().includes('contact') ||
k.toLowerCase().includes('client')
);
}
/**
* Normalizes a phone number (removes spaces, adds country code if needed)
*/
static normalizePhone(rawPhone: any): string | null {
if (!rawPhone) return null;
let phoneNumber = String(rawPhone)
.replace(/\s+/g, '')
.replace(/^\+/, '')
.replace(/\D/g, '');
if (phoneNumber.length < 7) return null;
// Senegal logic (9 digits -> add 221)
if (phoneNumber.length === 9) {
phoneNumber = `221${phoneNumber}`;
}
return phoneNumber;
}
/**
* Performs a bulk upsert of contacts and links them to a broadcast list
*/
static async bulkImport(organizationId: string, contacts: ContactImportRow[], listId: string): Promise<BulkImportResult> {
const results = { created: 0, updated: 0, errors: 0 };
for (const row of contacts) {
try {
const phoneKey = this.findPhoneKey(row);
const nameKey = this.findNameKey(row);
const phoneNumber = this.normalizePhone(phoneKey ? row[phoneKey] : null);
const name = nameKey ? String(row[nameKey]).trim() : null;
if (!phoneNumber) {
results.errors++;
continue;
}
const attributes: any = { ...row };
if (phoneKey) delete attributes[phoneKey];
if (nameKey) delete attributes[nameKey];
await prisma.contact.upsert({
where: {
phoneNumber_organizationId: { phoneNumber, organizationId }
},
update: {
name,
attributes,
broadcastLists: { connect: { id: listId } }
},
create: {
phoneNumber,
name,
attributes,
organizationId,
broadcastLists: { connect: { id: listId } }
}
});
results.created++;
} catch (err) {
logger.error({ err, row }, '[ContactService] Failed to import row');
results.errors++;
}
}
return results;
}
}