Download apps/api/src/services/ContactService.ts from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 3.5 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/services/ContactService.ts
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/api/src/services/ContactService.ts
-
curl -L -o ContactService.ts https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/api/src/services/ContactService.ts
3.5 kB
| 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; | |
| } | |
| } | |