import { crmStorage } from "./storage"; import type { CrmFrappeConnector, CrmLead, CrmOpportunity, CrmProduct, CrmPartner } from "@shared/schema"; interface FrappeResponse { data?: T; message?: string; exc_type?: string; } interface FrappeDocListItem { name: string; [key: string]: any; } export class FrappeService { private connector: CrmFrappeConnector; private baseUrl: string; private authHeader: string; constructor(connector: CrmFrappeConnector) { this.connector = connector; this.baseUrl = connector.baseUrl.replace(/\/$/, ""); this.authHeader = `token ${connector.apiKey}:${connector.apiSecret}`; } private async request( method: string, endpoint: string, data?: any ): Promise> { const url = `${this.baseUrl}${endpoint}`; const options: RequestInit = { method, headers: { Authorization: this.authHeader, "Content-Type": "application/json", Accept: "application/json", }, signal: AbortSignal.timeout(30000), }; if (data && (method === "POST" || method === "PUT")) { options.body = JSON.stringify(data); } const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`Frappe API error: ${response.status} - ${errorText}`); } return response.json(); } async testConnection(): Promise<{ success: boolean; user?: string; error?: string }> { try { const result = await this.request<{ message: string }>( "GET", "/api/method/frappe.auth.get_logged_user" ); return { success: true, user: result.message }; } catch (error: any) { return { success: false, error: error.message }; } } async getDocList(doctype: string, filters?: any[], fields?: string[], limit?: number): Promise { let endpoint = `/api/resource/${doctype}`; const params = new URLSearchParams(); if (filters) { params.append("filters", JSON.stringify(filters)); } if (fields) { params.append("fields", JSON.stringify(fields)); } if (limit) { params.append("limit_page_length", limit.toString()); } const queryString = params.toString(); if (queryString) { endpoint += `?${queryString}`; } const result = await this.request<{ data: FrappeDocListItem[] }>("GET", endpoint); return (result as any).data || []; } async getDoc(doctype: string, name: string): Promise { try { const result = await this.request<{ data: FrappeDocListItem }>( "GET", `/api/resource/${doctype}/${encodeURIComponent(name)}` ); return (result as any).data || null; } catch { return null; } } async createDoc(doctype: string, data: Record): Promise { const result = await this.request<{ data: FrappeDocListItem }>( "POST", `/api/resource/${doctype}`, data ); return (result as any).data!; } async updateDoc(doctype: string, name: string, data: Record): Promise { const result = await this.request<{ data: FrappeDocListItem }>( "PUT", `/api/resource/${doctype}/${encodeURIComponent(name)}`, data ); return (result as any).data!; } async syncLeadsToFrappe(leads: CrmLead[]): Promise<{ success: number; failed: number; errors: string[] }> { const errors: string[] = []; let success = 0; let failed = 0; for (const lead of leads) { try { const frappeData = { lead_name: lead.name, email_id: lead.email, phone: lead.phone, company_name: lead.company, job_title: lead.position, source: this.mapLeadSource(lead.source), notes: lead.notes, }; await this.createDoc("Lead", frappeData); success++; } catch (error: any) { failed++; errors.push(`Lead ${lead.name}: ${error.message}`); } } return { success, failed, errors }; } async syncOpportunitiesToFrappe(opportunities: CrmOpportunity[]): Promise<{ success: number; failed: number; errors: string[] }> { const errors: string[] = []; let success = 0; let failed = 0; for (const opp of opportunities) { try { const frappeData = { opportunity_type: "Sales", status: this.mapOpportunityStatus(opp.status), opportunity_amount: (opp.value || 0) / 100, opportunity_owner: opp.name, notes: opp.description, expected_closing: opp.expectedCloseDate, }; await this.createDoc("Opportunity", frappeData); success++; } catch (error: any) { failed++; errors.push(`Opportunity ${opp.name}: ${error.message}`); } } return { success, failed, errors }; } async syncProductsToFrappe(products: CrmProduct[]): Promise<{ success: number; failed: number; errors: string[] }> { const errors: string[] = []; let success = 0; let failed = 0; for (const product of products) { try { const frappeData = { item_code: product.sku || `ITEM-${product.id}`, item_name: product.name, description: product.description, item_group: product.category || "Products", is_stock_item: product.type === "product" ? 1 : 0, is_sales_item: 1, standard_rate: (product.price || 0) / 100, }; await this.createDoc("Item", frappeData); success++; } catch (error: any) { failed++; errors.push(`Product ${product.name}: ${error.message}`); } } return { success, failed, errors }; } async syncPartnersToFrappe(partners: CrmPartner[]): Promise<{ success: number; failed: number; errors: string[] }> { const errors: string[] = []; let success = 0; let failed = 0; for (const partner of partners) { try { const frappeData = { supplier_name: partner.name, supplier_type: "Partner", country: "Brazil", supplier_primary_contact: partner.primaryContactName, supplier_primary_address: partner.city && partner.state ? `${partner.city}, ${partner.state}` : undefined, }; await this.createDoc("Supplier", frappeData); success++; } catch (error: any) { failed++; errors.push(`Partner ${partner.name}: ${error.message}`); } } return { success, failed, errors }; } async pullLeadsFromFrappe(): Promise<{ leads: any[]; count: number }> { const leads = await this.getDocList("Lead", undefined, [ "name", "lead_name", "email_id", "phone", "company_name", "job_title", "source", "status", "notes" ], 100); return { leads, count: leads.length }; } async pullOpportunitiesFromFrappe(): Promise<{ opportunities: any[]; count: number }> { const opportunities = await this.getDocList("Opportunity", undefined, [ "name", "opportunity_type", "status", "opportunity_amount", "opportunity_owner", "expected_closing", "notes" ], 100); return { opportunities, count: opportunities.length }; } private mapLeadSource(source: string | null): string { const mapping: Record = { website: "Website", referral: "Reference", linkedin: "Social Media", event: "Campaign", other: "Others", }; return mapping[source || ""] || "Others"; } private mapOpportunityStatus(status: string | null): string { const mapping: Record = { open: "Open", won: "Converted", lost: "Lost", }; return mapping[status || ""] || "Open"; } } export async function createFrappeService(connectorId: number): Promise { const connector = await crmStorage.getFrappeConnector(connectorId); if (!connector) return null; return new FrappeService(connector); }