mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 10:07:22 +00:00
added script forms
This commit is contained in:
@@ -13,6 +13,7 @@ import { WorklistModule } from './worklist/worklist.module';
|
||||
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { SupervisorModule } from './supervisor/supervisor.module';
|
||||
import { EmbedModule } from './embed/embed.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,6 +33,7 @@ import { SupervisorModule } from './supervisor/supervisor.module';
|
||||
CallAssistModule,
|
||||
SearchModule,
|
||||
SupervisorModule,
|
||||
EmbedModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
27
src/embed/embed-cors.middleware.ts
Normal file
27
src/embed/embed-cors.middleware.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class EmbedCorsMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(EmbedCorsMiddleware.name);
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const origin = req.headers.origin || '*';
|
||||
this.logger.debug(`Embed CORS middleware - ${req.method} ${req.path} from ${origin}`);
|
||||
|
||||
// Set CORS headers for all requests
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization');
|
||||
res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours
|
||||
|
||||
// Handle preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
this.logger.debug('Handling OPTIONS preflight request');
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
18
src/embed/embed.module.ts
Normal file
18
src/embed/embed.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { PlatformModule } from '../platform/platform.module';
|
||||
import { LeadEmbedController } from './lead-embed.controller';
|
||||
import { EmbedCorsMiddleware } from './embed-cors.middleware';
|
||||
|
||||
@Module({
|
||||
imports: [PlatformModule],
|
||||
controllers: [LeadEmbedController],
|
||||
})
|
||||
export class EmbedModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(EmbedCorsMiddleware)
|
||||
.forRoutes(
|
||||
{ path: 'embed/*', method: RequestMethod.ALL }
|
||||
);
|
||||
}
|
||||
}
|
||||
176
src/embed/lead-embed.controller.ts
Normal file
176
src/embed/lead-embed.controller.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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>) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 createInitialActivity(
|
||||
leadId: string,
|
||||
body: Record<string, any>,
|
||||
authHeader: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const activityType = body.type === 'consultation' || body.type === 'appointment'
|
||||
? 'APPOINTMENT_BOOKED'
|
||||
: 'FORM_SUBMITTED';
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ async function bootstrap() {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
|
||||
const port = config.get('port');
|
||||
await app.listen(port);
|
||||
console.log(`Helix Engage Server running on port ${port}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
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()
|
||||
export class PlatformGraphqlService {
|
||||
@@ -120,6 +120,16 @@ export class PlatformGraphqlService {
|
||||
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) ---
|
||||
|
||||
async findLeadByPhoneWithToken(phone: string, authHeader: string): Promise<LeadNode | null> {
|
||||
|
||||
@@ -62,6 +62,19 @@ export type CreateLeadActivityInput = {
|
||||
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 = {
|
||||
leadStatus?: string;
|
||||
lastContactedAt?: string;
|
||||
|
||||
Reference in New Issue
Block a user