mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
Compare commits
4 Commits
dev-kartik
...
dev-mouli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff318dd10d | ||
|
|
2aef616ee3 | ||
|
|
09c7930b52 | ||
|
|
e912b982df |
@@ -1,6 +1,6 @@
|
|||||||
# Server
|
# Server
|
||||||
PORT=4100
|
PORT=4100
|
||||||
CORS_ORIGIN=http://localhost:5173
|
CORS_ORIGINS=http://localhost:5173,http://localhost:8000
|
||||||
|
|
||||||
# Fortytwo Platform
|
# Fortytwo Platform
|
||||||
PLATFORM_GRAPHQL_URL=http://localhost:4000/graphql
|
PLATFORM_GRAPHQL_URL=http://localhost:4000/graphql
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ import { WorklistModule } from './worklist/worklist.module';
|
|||||||
import { CallAssistModule } from './call-assist/call-assist.module';
|
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||||
import { SearchModule } from './search/search.module';
|
import { SearchModule } from './search/search.module';
|
||||||
import { SupervisorModule } from './supervisor/supervisor.module';
|
import { SupervisorModule } from './supervisor/supervisor.module';
|
||||||
import { MaintModule } from './maint/maint.module';
|
|
||||||
import { RecordingsModule } from './recordings/recordings.module';
|
|
||||||
import { EventsModule } from './events/events.module';
|
|
||||||
import { CallerResolutionModule } from './caller/caller-resolution.module';
|
|
||||||
import { RulesEngineModule } from './rules-engine/rules-engine.module';
|
|
||||||
import { ConfigThemeModule } from './config/config-theme.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -38,12 +32,6 @@ import { ConfigThemeModule } from './config/config-theme.module';
|
|||||||
CallAssistModule,
|
CallAssistModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
SupervisorModule,
|
SupervisorModule,
|
||||||
MaintModule,
|
|
||||||
RecordingsModule,
|
|
||||||
EventsModule,
|
|
||||||
CallerResolutionModule,
|
|
||||||
RulesEngineModule,
|
|
||||||
ConfigThemeModule,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
port: parseInt(process.env.PORT ?? '4100', 10),
|
port: parseInt(process.env.PORT ?? '4100', 10),
|
||||||
corsOrigin: process.env.CORS_ORIGIN ?? 'http://localhost:5173',
|
corsOrigins: (process.env.CORS_ORIGINS ?? 'http://localhost:5173')
|
||||||
|
.split(',')
|
||||||
|
.map(origin => origin.trim())
|
||||||
|
.filter(origin => origin.length > 0),
|
||||||
platform: {
|
platform: {
|
||||||
graphqlUrl: process.env.PLATFORM_GRAPHQL_URL ?? 'http://localhost:4000/graphql',
|
graphqlUrl: process.env.PLATFORM_GRAPHQL_URL ?? 'http://localhost:4000/graphql',
|
||||||
apiKey: process.env.PLATFORM_API_KEY ?? '',
|
apiKey: process.env.PLATFORM_API_KEY ?? '',
|
||||||
|
|||||||
9
src/embed/embed.module.ts
Normal file
9
src/embed/embed.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PlatformModule } from '../platform/platform.module';
|
||||||
|
import { LeadEmbedController } from './lead-embed.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PlatformModule],
|
||||||
|
controllers: [LeadEmbedController],
|
||||||
|
})
|
||||||
|
export class EmbedModule {}
|
||||||
227
src/embed/lead-embed.controller.ts
Normal file
227
src/embed/lead-embed.controller.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common';
|
||||||
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Controller('embed/leads')
|
||||||
|
export class LeadEmbedController {
|
||||||
|
private readonly logger = new Logger(LeadEmbedController.name);
|
||||||
|
private readonly apiKey: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly platform: PlatformGraphqlService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {
|
||||||
|
this.apiKey = config.get<string>('platform.apiKey') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('create')
|
||||||
|
async handleLeadCreation(@Body() body: Record<string, any>) {
|
||||||
|
console.log("Lead creation from embed received:", body);
|
||||||
|
this.logger.log(`Lead creation from embed received: ${JSON.stringify(body)}`);
|
||||||
|
|
||||||
|
const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : '';
|
||||||
|
if (!authHeader) {
|
||||||
|
this.logger.warn('No PLATFORM_API_KEY configured — cannot create lead');
|
||||||
|
throw new HttpException('Server configuration error', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const leadData = this.mapIncomingDataToLead(body);
|
||||||
|
|
||||||
|
if (!leadData.contactPhone && !leadData.contactEmail) {
|
||||||
|
throw new HttpException('Either contact phone or email is required', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up campaign by name and link via relation if not already set by ID
|
||||||
|
if (!leadData.campaignId) {
|
||||||
|
const campaignName = body.utm_campaign || body.utmCampaign || body.campaign;
|
||||||
|
if (campaignName) {
|
||||||
|
const campaignId = await this.lookupCampaignByName(campaignName, authHeader);
|
||||||
|
if (campaignId) {
|
||||||
|
leadData.campaignId = campaignId;
|
||||||
|
this.logger.log(`Matched campaign "${campaignName}" → ${campaignId}`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`No campaign found matching name: "${campaignName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.platform.queryWithAuth<any>(
|
||||||
|
`mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`,
|
||||||
|
{ data: leadData },
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
|
||||||
|
const leadId = result.createLead.id;
|
||||||
|
this.logger.log(`Lead created successfully: ${leadId}`);
|
||||||
|
|
||||||
|
if (body.notes || body.type) {
|
||||||
|
await this.createInitialActivity(leadId, body, authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
leadId,
|
||||||
|
message: 'Lead created successfully',
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
const responseData = error?.response?.data ? JSON.stringify(error.response.data) : '';
|
||||||
|
this.logger.error(`Lead creation failed: ${error.message} ${responseData}`);
|
||||||
|
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Lead creation failed',
|
||||||
|
error.response?.status || 500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapIncomingDataToLead(body: Record<string, any>): Record<string, any> {
|
||||||
|
const leadData: Record<string, any> = {};
|
||||||
|
|
||||||
|
const contactName = body.contact_name || body.contactName || 'Unknown';
|
||||||
|
const nameParts = contactName.split(' ');
|
||||||
|
const firstName = nameParts[0] || 'Unknown';
|
||||||
|
const lastName = nameParts.slice(1).join(' ');
|
||||||
|
|
||||||
|
leadData.name = contactName;
|
||||||
|
leadData.contactName = {
|
||||||
|
firstName,
|
||||||
|
lastName: lastName || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.contact_phone || body.contactPhone) {
|
||||||
|
const phone = body.contact_phone || body.contactPhone;
|
||||||
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
|
leadData.contactPhone = {
|
||||||
|
primaryPhoneNumber: cleanPhone.startsWith('91') ? `+${cleanPhone}` : `+91${cleanPhone}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.contact_email || body.contactEmail) {
|
||||||
|
leadData.contactEmail = {
|
||||||
|
primaryEmail: body.contact_email || body.contactEmail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
leadData.source = body.source || 'WEBSITE';
|
||||||
|
leadData.status = body.lead_status || body.status || 'NEW';
|
||||||
|
|
||||||
|
const interestedService = this.mapInterestedService(body);
|
||||||
|
if (interestedService) {
|
||||||
|
leadData.interestedService = interestedService;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.assigned_agent || body.assignedAgent) {
|
||||||
|
leadData.assignedAgent = body.assigned_agent || body.assignedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.campaign_id || body.campaignId) {
|
||||||
|
leadData.campaignId = body.campaign_id || body.campaignId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTM tracking fields
|
||||||
|
const utmCampaign = body.utm_campaign || body.utmCampaign || body.campaign;
|
||||||
|
if (utmCampaign) {
|
||||||
|
leadData.utmCampaign = utmCampaign;
|
||||||
|
}
|
||||||
|
if (body.utm_source || body.utmSource) {
|
||||||
|
leadData.utmSource = body.utm_source || body.utmSource;
|
||||||
|
}
|
||||||
|
if (body.utm_medium || body.utmMedium) {
|
||||||
|
leadData.utmMedium = body.utm_medium || body.utmMedium;
|
||||||
|
}
|
||||||
|
if (body.utm_content || body.utmContent) {
|
||||||
|
leadData.utmContent = body.utm_content || body.utmContent;
|
||||||
|
}
|
||||||
|
if (body.utm_term || body.utmTerm) {
|
||||||
|
leadData.utmTerm = body.utm_term || body.utmTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leadData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapInterestedService(body: Record<string, any>): string | null {
|
||||||
|
const type = body.type || body.interested_service || body.interestedService;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return body.department || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceMap: Record<string, string> = {
|
||||||
|
'consultation': 'Appointment',
|
||||||
|
'follow_up': 'Appointment',
|
||||||
|
'procedure': 'Appointment',
|
||||||
|
'emergency': 'Appointment',
|
||||||
|
'general_enquiry': 'General Enquiry',
|
||||||
|
'general': 'General Enquiry',
|
||||||
|
};
|
||||||
|
|
||||||
|
return serviceMap[type.toLowerCase()] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async lookupCampaignByName(name: string, authHeader: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ campaigns(first: 50) { edges { node { id campaignName } } } }`,
|
||||||
|
undefined,
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
const campaigns = data?.campaigns?.edges?.map((e: any) => e.node) ?? [];
|
||||||
|
const match = campaigns.find(
|
||||||
|
(c: any) => (c.campaignName ?? '').toLowerCase() === name.toLowerCase(),
|
||||||
|
);
|
||||||
|
return match?.id ?? null;
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Campaign lookup failed: ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createInitialActivity(
|
||||||
|
leadId: string,
|
||||||
|
body: Record<string, any>,
|
||||||
|
authHeader: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const activityType = body.type === 'consultation' || body.type === 'appointment'
|
||||||
|
? 'APPOINTMENT_BOOKED'
|
||||||
|
: 'CALL_RECEIVED';
|
||||||
|
|
||||||
|
let summary = 'Lead submitted via web form';
|
||||||
|
if (body.type) {
|
||||||
|
summary = `${body.type.replace(/_/g, ' ')} requested`;
|
||||||
|
}
|
||||||
|
if (body.department) {
|
||||||
|
summary += ` - ${body.department}`;
|
||||||
|
}
|
||||||
|
if (body.title) {
|
||||||
|
summary += ` (from ${body.title})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.platform.queryWithAuth<any>(
|
||||||
|
`mutation($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
name: summary.substring(0, 80),
|
||||||
|
activityType,
|
||||||
|
summary,
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
performedBy: 'System',
|
||||||
|
channel: 'PHONE',
|
||||||
|
leadId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Initial activity created for lead ${leadId}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorDetails = error?.response?.data ? JSON.stringify(error.response.data) : error.message;
|
||||||
|
this.logger.error(`Failed to create initial activity: ${errorDetails}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,13 @@ async function bootstrap() {
|
|||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
|
|
||||||
|
const corsOrigins = config.get<string[]>('corsOrigins') || ['http://localhost:5173'];
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: config.get('corsOrigin'),
|
origin: corsOrigins,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Accept', 'Authorization'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = config.get('port');
|
const port = config.get('port');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { LeadNode, LeadActivityNode, CreateCallInput, CreateLeadActivityInput, UpdateLeadInput } from './platform.types';
|
import type { LeadNode, LeadActivityNode, CreateCallInput, CreateLeadActivityInput, CreateLeadInput, UpdateLeadInput } from './platform.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlatformGraphqlService {
|
export class PlatformGraphqlService {
|
||||||
@@ -120,6 +120,16 @@ export class PlatformGraphqlService {
|
|||||||
return data.createLeadActivity;
|
return data.createLeadActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createLead(input: CreateLeadInput): Promise<{ id: string }> {
|
||||||
|
const data = await this.query<{ createLead: { id: string } }>(
|
||||||
|
`mutation CreateLead($data: LeadCreateInput!) {
|
||||||
|
createLead(data: $data) { id }
|
||||||
|
}`,
|
||||||
|
{ data: input },
|
||||||
|
);
|
||||||
|
return data.createLead;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Token passthrough versions (for user-driven requests) ---
|
// --- Token passthrough versions (for user-driven requests) ---
|
||||||
|
|
||||||
async findLeadByPhoneWithToken(phone: string, authHeader: string): Promise<LeadNode | null> {
|
async findLeadByPhoneWithToken(phone: string, authHeader: string): Promise<LeadNode | null> {
|
||||||
|
|||||||
@@ -63,6 +63,19 @@ export type CreateLeadActivityInput = {
|
|||||||
leadId: string;
|
leadId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateLeadInput = {
|
||||||
|
name: string;
|
||||||
|
contactName?: { firstName: string; lastName?: string };
|
||||||
|
contactPhone?: { primaryPhoneNumber: string };
|
||||||
|
contactEmail?: { primaryEmailAddress: string };
|
||||||
|
source?: string;
|
||||||
|
status?: string;
|
||||||
|
interestedService?: string;
|
||||||
|
assignedAgent?: string;
|
||||||
|
campaignId?: string;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateLeadInput = {
|
export type UpdateLeadInput = {
|
||||||
leadStatus?: string;
|
leadStatus?: string;
|
||||||
lastContactedAt?: string;
|
lastContactedAt?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user