arcadiasuite/server/crm/frappe-service.ts

272 lines
7.9 KiB
TypeScript

import { crmStorage } from "./storage";
import type { CrmFrappeConnector, CrmLead, CrmOpportunity, CrmProduct, CrmPartner } from "@shared/schema";
interface FrappeResponse<T = any> {
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<T>(
method: string,
endpoint: string,
data?: any
): Promise<FrappeResponse<T>> {
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<FrappeDocListItem[]> {
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<FrappeDocListItem | null> {
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<string, any>): Promise<FrappeDocListItem> {
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<string, any>): Promise<FrappeDocListItem> {
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<string, string> = {
website: "Website",
referral: "Reference",
linkedin: "Social Media",
event: "Campaign",
other: "Others",
};
return mapping[source || ""] || "Others";
}
private mapOpportunityStatus(status: string | null): string {
const mapping: Record<string, string> = {
open: "Open",
won: "Converted",
lost: "Lost",
};
return mapping[status || ""] || "Open";
}
}
export async function createFrappeService(connectorId: number): Promise<FrappeService | null> {
const connector = await crmStorage.getFrappeConnector(connectorId);
if (!connector) return null;
return new FrappeService(connector);
}