3 Commits

Author SHA1 Message Date
Kartik Datrika
05eb7a326e Merge branch 'dev' into dev-main 2026-04-06 11:15:03 +05:30
moulichand16
09c7930b52 fixed cors 2026-03-27 16:05:18 +05:30
moulichand16
e912b982df added script forms 2026-03-27 10:53:20 +05:30
7 changed files with 220 additions and 4 deletions

View File

@@ -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

View File

@@ -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 ?? '',

View 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 {}

View File

@@ -0,0 +1,177 @@
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);
}
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'
: '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}`);
}
}
}

View File

@@ -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');

View File

@@ -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> {

View File

@@ -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;