feat: Implement Salesforce Nonprofit Cloud CRM integration and Real-Time Data Processing system
- Added SalesforceConnector for handling Salesforce authentication, case creation, and updates. - Integrated real-time processing capabilities with StudentAssistanceAI and Salesforce. - Implemented WebSocket support for real-time updates and request handling. - Enhanced metrics tracking for processing performance and sync status. - Added error handling and retry logic for processing requests. - Created factory function for easy initialization of RealTimeProcessor with default configurations.
This commit is contained in:
312
src/crm/SalesforceConnector.ts
Normal file
312
src/crm/SalesforceConnector.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
// Phase 3B: Salesforce Nonprofit Cloud CRM Integration
|
||||
import type { StudentRequest, MatchResult } from '../ai/types'
|
||||
|
||||
export interface SalesforceConfig {
|
||||
instanceUrl: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
username: string
|
||||
password: string
|
||||
securityToken: string
|
||||
apiVersion: string
|
||||
}
|
||||
|
||||
export interface SalesforceContact {
|
||||
Id: string
|
||||
Name: string
|
||||
Email: string
|
||||
Phone: string
|
||||
Account: {
|
||||
Id: string
|
||||
Name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceCase {
|
||||
Id: string
|
||||
Subject: string
|
||||
Description: string
|
||||
Status: 'New' | 'In Progress' | 'Closed' | 'Escalated'
|
||||
Priority: 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
ContactId: string
|
||||
CaseNumber: string
|
||||
CreatedDate: string
|
||||
LastModifiedDate: string
|
||||
}
|
||||
|
||||
export interface NPSPAllocation {
|
||||
Id: string
|
||||
Amount: number
|
||||
GAU__c: string // General Accounting Unit
|
||||
Opportunity__c: string
|
||||
Percent: number
|
||||
}
|
||||
|
||||
class SalesforceConnector {
|
||||
private config: SalesforceConfig
|
||||
private accessToken: string | null = null
|
||||
private instanceUrl: string = ''
|
||||
|
||||
constructor(config: SalesforceConfig) {
|
||||
this.config = config
|
||||
this.instanceUrl = config.instanceUrl
|
||||
}
|
||||
|
||||
// Authentication with Salesforce
|
||||
async authenticate(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.config.instanceUrl}/services/oauth2/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'password',
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
username: this.config.username,
|
||||
password: this.config.password + this.config.securityToken
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authentication failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
this.accessToken = data.access_token
|
||||
this.instanceUrl = data.instance_url
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Salesforce authentication error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Create assistance request case in Salesforce
|
||||
async createAssistanceCase(request: StudentRequest): Promise<string | null> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const caseData = {
|
||||
Subject: `Student Assistance Request - ${request.category}`,
|
||||
Description: this.formatRequestDescription(request),
|
||||
Status: 'New',
|
||||
Priority: this.determinePriority(request),
|
||||
Origin: 'AI Portal',
|
||||
Type: 'Student Assistance',
|
||||
// Custom fields for nonprofit
|
||||
Student_Name__c: request.studentName,
|
||||
Student_ID__c: request.studentId,
|
||||
Need_Category__c: request.category,
|
||||
Urgency_Level__c: request.urgency,
|
||||
Location_City__c: request.location.city,
|
||||
Location_State__c: request.location.state,
|
||||
Location_Zip__c: request.location.zipCode
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(caseData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create case: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.id
|
||||
} catch (error) {
|
||||
console.error('Error creating Salesforce case:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Update case with AI matching results
|
||||
async updateCaseWithMatching(caseId: string, matchResult: MatchResult): Promise<boolean> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
AI_Match_Confidence__c: matchResult.confidenceScore,
|
||||
Recommended_Resource_Id__c: matchResult.resourceId,
|
||||
Recommended_Resource_Name__c: matchResult.resourceName,
|
||||
Resource_Type__c: matchResult.resourceType,
|
||||
Estimated_Impact__c: matchResult.estimatedImpact,
|
||||
Estimated_Cost__c: matchResult.estimatedCost,
|
||||
Fulfillment_Timeline__c: matchResult.fulfillmentTimeline,
|
||||
Last_AI_Update__c: new Date().toISOString()
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case/${caseId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
})
|
||||
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('Error updating Salesforce case:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Get nonprofit contacts (volunteers, donors, partners)
|
||||
async getContacts(recordType?: string): Promise<SalesforceContact[]> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
let query = `SELECT Id, Name, Email, Phone, Account.Id, Account.Name FROM Contact`
|
||||
|
||||
if (recordType) {
|
||||
query += ` WHERE RecordType.Name = '${recordType}'`
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch contacts: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.records
|
||||
} catch (error) {
|
||||
console.error('Error fetching Salesforce contacts:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Create NPSP allocation for resource tracking
|
||||
async createResourceAllocation(opportunityId: string, amount: number, gauId: string): Promise<string | null> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationData = {
|
||||
Amount__c: amount,
|
||||
GAU__c: gauId,
|
||||
Opportunity__c: opportunityId,
|
||||
Percent__c: 100
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Allocation__c`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(allocationData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create allocation: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.id
|
||||
} catch (error) {
|
||||
console.error('Error creating resource allocation:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Get donation opportunities for matching
|
||||
async getDonationOpportunities(category?: string): Promise<any[]> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
let query = `SELECT Id, Name, Amount, StageName, CloseDate, Account.Name
|
||||
FROM Opportunity
|
||||
WHERE StageName IN ('Pledged', 'Posted')
|
||||
AND CloseDate >= TODAY`
|
||||
|
||||
if (category) {
|
||||
query += ` AND Category__c = '${category}'`
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch opportunities: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.records
|
||||
} catch (error) {
|
||||
console.error('Error fetching donation opportunities:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private formatRequestDescription(request: StudentRequest): string {
|
||||
return `
|
||||
AI-Generated Student Assistance Request
|
||||
|
||||
Student Information:
|
||||
- Name: ${request.studentName}
|
||||
- ID: ${request.studentId}
|
||||
- Location: ${request.location.city}, ${request.location.state} ${request.location.zipCode}
|
||||
|
||||
Request Details:
|
||||
- Category: ${request.category}
|
||||
- Urgency: ${request.urgency}
|
||||
- Description: ${request.description}
|
||||
|
||||
Additional Information:
|
||||
- Estimated Cost: $${request.estimatedCost || 0}
|
||||
- Required Skills: ${request.requiredSkills?.join(', ') || 'None specified'}
|
||||
- Deadline: ${request.deadline ? new Date(request.deadline).toLocaleDateString() : 'Not specified'}
|
||||
|
||||
Submission Details:
|
||||
- Submitted: ${new Date(request.submittedAt).toLocaleString()}
|
||||
- Request ID: ${request.id}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
private determinePriority(request: StudentRequest): 'Low' | 'Medium' | 'High' | 'Critical' {
|
||||
switch (request.urgency) {
|
||||
case 'emergency':
|
||||
return 'Critical'
|
||||
case 'high':
|
||||
return 'High'
|
||||
case 'medium':
|
||||
return 'Medium'
|
||||
case 'low':
|
||||
default:
|
||||
return 'Low'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SalesforceConnector }
|
||||
Reference in New Issue
Block a user