fix(zoho): fix OAuth token endpoint, improve lead field mapping, add upsert
- Fix critical bug: token refresh now uses ZOHO_ACCOUNTS_DOMAIN (accounts.zoho.com) instead of API domain (www.zohoapis.com). The OAuth token endpoint lives on a different domain. - Remove unnecessary redirect_uri from refresh token request - Add ZOHO_ACCOUNTS_DOMAIN env var (separate from API domain) - Split contact name into First_Name/Last_Name for Zoho schema - Replace Service_Interest (non-standard field) with Description + Lead_Source: Website (standard picklist value) - Switch from Insert to Upsert API with duplicate_check_fields: [Email] so duplicate submissions update instead of error - Add trigger: ['workflow'] for explicit workflow control - Add token refresh retry (1 retry on transient failure) - Add ZOHO_CASES_ENABLED env var for future Cases forwarding - Update .env.example with full Zoho config documentation - Update FUTURE.md with detailed Phase 7 Zoho integration plan - Remove obsolete ZOHO_REDIRECT_URI from Dockerfile
This commit is contained in:
parent
f1823bcc4b
commit
debde23ab7
11
.env.example
11
.env.example
|
|
@ -3,3 +3,14 @@
|
|||
|
||||
NODE_ENV=production
|
||||
SERVER_PORT=3001
|
||||
|
||||
# Zoho CRM Integration
|
||||
# Set ZOHO_ENABLED=true to forward leads/support to Zoho CRM
|
||||
# Get credentials from https://api-console.zoho.com → Self Client
|
||||
ZOHO_ENABLED=false
|
||||
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||
ZOHO_CLIENT_ID=
|
||||
ZOHO_CLIENT_SECRET=
|
||||
ZOHO_REFRESH_TOKEN=
|
||||
ZOHO_CASES_ENABLED=false
|
||||
|
|
|
|||
|
|
@ -45,10 +45,11 @@ ENV CORS_ORIGIN=*
|
|||
ENV LOG_LEVEL=info
|
||||
ENV ZOHO_ENABLED=false
|
||||
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||
ENV ZOHO_CLIENT_ID=
|
||||
ENV ZOHO_CLIENT_SECRET=
|
||||
ENV ZOHO_REFRESH_TOKEN=
|
||||
ENV ZOHO_REDIRECT_URI=
|
||||
ENV ZOHO_CASES_ENABLED=false
|
||||
|
||||
# Create app directory structure
|
||||
RUN mkdir -p /app/db /app/logs
|
||||
|
|
|
|||
|
|
@ -213,10 +213,10 @@ const supportSchema = z.object({
|
|||
// --- Zoho CRM Forwarding (best-effort, fire-and-forget) ---
|
||||
const ZOHO_ENABLED = process.env.ZOHO_ENABLED === 'true'
|
||||
const ZOHO_API_DOMAIN = process.env.ZOHO_API_DOMAIN || 'https://www.zohoapis.com'
|
||||
const ZOHO_ACCOUNTS_DOMAIN = process.env.ZOHO_ACCOUNTS_DOMAIN || 'https://accounts.zoho.com'
|
||||
const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null
|
||||
const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
|
||||
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
|
||||
const ZOHO_REDIRECT_URI = process.env.ZOHO_REDIRECT_URI || ''
|
||||
|
||||
// In-memory access token cache
|
||||
let zohoAccessToken = null
|
||||
|
|
@ -232,13 +232,14 @@ async function getZohoAccessToken() {
|
|||
}
|
||||
|
||||
try {
|
||||
const url = `${ZOHO_API_DOMAIN}/oauth/v2/token`
|
||||
// Token endpoint is on the ACCOUNTS domain, NOT the API domain
|
||||
// US: accounts.zoho.com | EU: accounts.zoho.eu | IN: accounts.zoho.in
|
||||
const url = `${ZOHO_ACCOUNTS_DOMAIN}/oauth/v2/token`
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: ZOHO_CLIENT_ID,
|
||||
client_secret: ZOHO_CLIENT_SECRET,
|
||||
refresh_token: ZOHO_REFRESH_TOKEN,
|
||||
redirect_uri: ZOHO_REDIRECT_URI,
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
|
|
@ -297,26 +298,51 @@ async function forwardToZoho(leadData) {
|
|||
return
|
||||
}
|
||||
|
||||
const accessToken = await getZohoAccessToken()
|
||||
let accessToken = await getZohoAccessToken()
|
||||
if (!accessToken) {
|
||||
log.warn("[Zoho] No access token available, skipping lead forwarding")
|
||||
return
|
||||
// Retry once — token refresh can fail transiently
|
||||
log.warn('[Zoho] First token refresh failed, retrying...')
|
||||
// Clear cached token to force a fresh attempt
|
||||
zohoAccessToken = null
|
||||
zohoTokenExpiry = 0
|
||||
accessToken = await getZohoAccessToken()
|
||||
if (!accessToken) {
|
||||
log.warn('[Zoho] No access token available after retry, skipping lead forwarding')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #8: Prevent double-slash in URL path
|
||||
const url = `${ZOHO_API_DOMAIN.replace(/\/$/, "")}/crm/v8/Leads`
|
||||
// Use upsert to handle duplicates gracefully (insert new or update existing by email)
|
||||
const url = `${ZOHO_API_DOMAIN.replace(/\/$/, "")}/crm/v8/Leads/upsert`
|
||||
|
||||
// Split full name into First_Name / Last_Name for Zoho
|
||||
// Zoho requires Last_Name (mandatory), First_Name is optional
|
||||
const nameParts = (leadData.name || '').trim().split(/\s+/)
|
||||
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : (nameParts[0] || 'Unknown')
|
||||
const firstName = nameParts.length > 1 ? nameParts[0] : ''
|
||||
|
||||
// Build Description with service interest appended for Zoho visibility
|
||||
const descriptionParts = []
|
||||
if (leadData.message) descriptionParts.push(leadData.message)
|
||||
if (leadData.service_interest) descriptionParts.push(`Service Interest: ${leadData.service_interest}`)
|
||||
const description = descriptionParts.join('\n\n')
|
||||
|
||||
const payload = {
|
||||
data: [
|
||||
{
|
||||
Company: leadData.company || "",
|
||||
Last_Name: leadData.name || "Unknown",
|
||||
Email: leadData.email || "",
|
||||
Phone: leadData.phone || "",
|
||||
Zip_Code: leadData.zip || "",
|
||||
Description: leadData.message || "",
|
||||
Service_Interest: leadData.service_interest || null,
|
||||
First_Name: firstName || undefined,
|
||||
Last_Name: lastName,
|
||||
Company: leadData.company || '',
|
||||
Email: leadData.email || '',
|
||||
Phone: leadData.phone || '',
|
||||
Zip_Code: leadData.zip || '',
|
||||
Description: description || '',
|
||||
Lead_Source: 'Website',
|
||||
},
|
||||
],
|
||||
duplicate_check_fields: ['Email'],
|
||||
trigger: ['workflow'],
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
|
@ -552,7 +578,10 @@ app.listen(PORT, () => {
|
|||
log.info(`Server running on http://localhost:${PORT}`)
|
||||
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
||||
if (ZOHO_ENABLED) {
|
||||
log.info(`Zoho CRM forwarding: ENABLED (domain: ${ZOHO_API_DOMAIN})`)
|
||||
log.info(`Zoho CRM forwarding: ENABLED`)
|
||||
log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`)
|
||||
log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`)
|
||||
log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`)
|
||||
} else {
|
||||
log.info('Zoho CRM forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue