import fetch from "node-fetch"; const ERPNEXT_URL = process.env.ERPNEXT_URL || ""; const ERPNEXT_API_KEY = process.env.ERPNEXT_API_KEY || ""; const ERPNEXT_API_SECRET = process.env.ERPNEXT_API_SECRET || ""; interface ERPNextResponse { data: T; message?: string; } interface DocTypeField { name: string; label?: string; fieldtype: string; options?: string; reqd?: number; } interface DocTypeInfo { name: string; module?: string; is_submittable?: number; is_tree?: number; istable?: number; } function getAuthHeader(): Record { return { "Authorization": `token ${ERPNEXT_API_KEY}:${ERPNEXT_API_SECRET}`, "Content-Type": "application/json", }; } function encodeDocType(doctype: string): string { return encodeURIComponent(doctype); } export async function testConnection(): Promise<{ success: boolean; message: string; user?: string }> { try { const response = await fetch(`${ERPNEXT_URL}/api/method/frappe.auth.get_logged_user`, { method: "GET", headers: getAuthHeader(), }); if (!response.ok) { return { success: false, message: `Erro de conexão: ${response.status} ${response.statusText}` }; } const data = await response.json() as ERPNextResponse; return { success: true, message: "Conexão estabelecida com sucesso", user: data.message }; } catch (error: unknown) { const message = error instanceof Error ? error.message : "Erro desconhecido"; return { success: false, message: `Erro ao conectar: ${message}` }; } } export async function listDocTypes(limit: number = 100): Promise { const response = await fetch( `${ERPNEXT_URL}/api/resource/DocType?fields=["name","module","is_submittable","is_tree","istable"]&limit_page_length=${limit}`, { method: "GET", headers: getAuthHeader(), } ); if (!response.ok) { throw new Error(`Erro ao listar DocTypes: ${response.status}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function getDocTypeFields(doctype: string): Promise { const response = await fetch( `${ERPNEXT_URL}/api/resource/DocType/${encodeDocType(doctype)}`, { method: "GET", headers: getAuthHeader(), } ); if (!response.ok) { throw new Error(`Erro ao buscar campos do DocType ${doctype}: ${response.status}`); } const data = await response.json() as ERPNextResponse<{ fields: DocTypeField[] }>; return data.data.fields || []; } export async function getDocuments( doctype: string, filters?: Record, fields?: string[], limit: number = 20, orderBy?: string ): Promise { let url = `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}?limit_page_length=${limit}`; if (fields && fields.length > 0) { url += `&fields=${encodeURIComponent(JSON.stringify(fields))}`; } if (filters && Object.keys(filters).length > 0) { url += `&filters=${encodeURIComponent(JSON.stringify(filters))}`; } if (orderBy) { url += `&order_by=${encodeURIComponent(orderBy)}`; } const response = await fetch(url, { method: "GET", headers: getAuthHeader(), }); if (!response.ok) { throw new Error(`Erro ao buscar documentos de ${doctype}: ${response.status}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function getDocument(doctype: string, name: string): Promise { const response = await fetch( `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`, { method: "GET", headers: getAuthHeader(), } ); if (!response.ok) { throw new Error(`Erro ao buscar documento ${doctype}/${name}: ${response.status}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function createDocument( doctype: string, documentData: Record ): Promise { const response = await fetch( `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}`, { method: "POST", headers: getAuthHeader(), body: JSON.stringify(documentData), } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`Erro ao criar documento ${doctype}: ${response.status} - ${errorText}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function updateDocument( doctype: string, name: string, documentData: Record ): Promise { const response = await fetch( `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`, { method: "PUT", headers: getAuthHeader(), body: JSON.stringify(documentData), } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`Erro ao atualizar documento ${doctype}/${name}: ${response.status} - ${errorText}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function deleteDocument(doctype: string, name: string): Promise<{ success: boolean; message: string }> { const response = await fetch( `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}/${encodeURIComponent(name)}`, { method: "DELETE", headers: getAuthHeader(), } ); if (!response.ok) { throw new Error(`Erro ao deletar documento ${doctype}/${name}: ${response.status}`); } return { success: true, message: `Documento ${name} deletado com sucesso` }; } export async function runReport( reportName: string, filters?: Record ): Promise<{ columns: unknown[]; data: unknown[] }> { let url = `${ERPNEXT_URL}/api/method/frappe.desk.query_report.run?report_name=${encodeURIComponent(reportName)}`; if (filters && Object.keys(filters).length > 0) { url += `&filters=${encodeURIComponent(JSON.stringify(filters))}`; } const response = await fetch(url, { method: "GET", headers: getAuthHeader(), }); if (!response.ok) { throw new Error(`Erro ao executar relatório ${reportName}: ${response.status}`); } const data = await response.json() as ERPNextResponse<{ result: unknown[]; columns: unknown[] }>; return { columns: data.message ? (data as unknown as { message: { columns: unknown[] } }).message.columns : [], data: data.message ? (data as unknown as { message: { result: unknown[] } }).message.result : [], }; } export async function searchDocuments( doctype: string, searchTerm: string, fields?: string[], limit: number = 20 ): Promise { const searchFilters: (string | unknown)[][] = []; const doctypeFields = await getDocTypeFields(doctype); const textFields = doctypeFields .filter(f => ["Data", "Text", "Small Text", "Long Text", "Text Editor"].includes(f.fieldtype)) .slice(0, 3) .map(f => f.name); if (textFields.length > 0) { textFields.forEach(field => { searchFilters.push([doctype, field, "like", `%${searchTerm}%`]); }); } const orFilters: (string | unknown)[][] = searchFilters.length > 0 ? searchFilters : [[doctype, "name", "like", `%${searchTerm}%`]]; let url = `${ERPNEXT_URL}/api/resource/${encodeDocType(doctype)}?limit_page_length=${limit}`; url += `&or_filters=${encodeURIComponent(JSON.stringify(orFilters))}`; if (fields && fields.length > 0) { url += `&fields=${encodeURIComponent(JSON.stringify(fields))}`; } const response = await fetch(url, { method: "GET", headers: getAuthHeader(), }); if (!response.ok) { throw new Error(`Erro na busca em ${doctype}: ${response.status}`); } const data = await response.json() as ERPNextResponse; return data.data; } export async function callMethod( method: string, args?: Record ): Promise { const response = await fetch( `${ERPNEXT_URL}/api/method/${method}`, { method: "POST", headers: getAuthHeader(), body: args ? JSON.stringify(args) : undefined, } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`Erro ao chamar método ${method}: ${response.status} - ${errorText}`); } const data = await response.json() as ERPNextResponse; return data.message as T || data.data; } export function isConfigured(): boolean { return Boolean(ERPNEXT_URL && ERPNEXT_API_KEY && ERPNEXT_API_SECRET); } export function getConfig(): { url: string; configured: boolean } { return { url: ERPNEXT_URL, configured: isConfigured(), }; }