import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" // Lexware lexoffice API configuration const LEXOFFICE_API_URL = "https://api.lexware.io" const LEXOFFICE_API_KEY = "EVrdrnU7rEhJp93WAJ8MhwPAP.tYOwKTj27Ep2D8fTelLnIJ" // Log collector for debugging const createLogger = () => { const logs: string[] = [] return { log: (message: string) => { const timestamp = new Date().toISOString() logs.push(`[${timestamp}] ${message}`) }, getLogs: () => logs } } // Helper to make lexoffice API calls const lexofficeRequest = async (endpoint: string, method: string, body?: any, logger?: ReturnType) => { const headers: Record = { 'Authorization': `Bearer ${LEXOFFICE_API_KEY}`, 'Accept': 'application/json' } if (body) { headers['Content-Type'] = 'application/json' } const options: any = { method, headers } if (body) { options.body = JSON.stringify(body) } logger?.log(`Lexoffice API: ${method} ${endpoint}`) const response = await fetch(`${LEXOFFICE_API_URL}${endpoint}`, options) if (!response.ok) { const errorText = await response.text() logger?.log(`Lexoffice API error (${response.status}): ${errorText}`) throw new Error(`Lexoffice API error (${response.status}): ${errorText}`) } const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { const data = await response.json() logger?.log(`Lexoffice API response: ${JSON.stringify(data).substring(0, 200)}...`) return data } const locationHeader = response.headers.get('Location') if (locationHeader) { const id = locationHeader.split('/').pop() logger?.log(`Lexoffice API: Created resource with ID ${id}`) return { id, status: response.status } } return { status: response.status, statusText: response.statusText } } // Query lexoffice contacts by name const findContact = async (name: string, logger?: ReturnType): Promise => { try { if (name && name.length >= 3) { logger?.log(`Searching for contact: "${name}"`) const result = await lexofficeRequest(`/v1/contacts?name=${encodeURIComponent(name)}`, 'GET', undefined, logger) if (result?.content?.length > 0) { logger?.log(`Found existing contact: ${result.content[0].id}`) return result.content[0] } logger?.log(`No contact found for: "${name}"`) } return null } catch (err: any) { logger?.log(`Error searching contact: ${err.message}`) return null } } // Create a new contact in lexoffice const createLexofficeContact = async (partner: any, logger?: ReturnType): Promise => { const partnerName = partner.Name || '' const partnerName2 = partner.Name2 || '' const isCustomer = partner.IsCustomer === true || partner.IsCustomer === 'Y' const isVendor = partner.IsVendor === true || partner.IsVendor === 'Y' logger?.log(`Creating contact for: "${partnerName}" (Customer: ${isCustomer}, Vendor: ${isVendor})`) const roles: any = {} if (isCustomer) roles.customer = {} if (isVendor) roles.vendor = {} if (!isCustomer && !isVendor) roles.customer = {} let contactData: any = { version: 0, roles } if (partner.EMail) { contactData.emailAddresses = { business: [partner.EMail] } } // Use "LogShip" instead of "iDempiere", include customer number (Value) contactData.note = `LogShip Customer#: ${partner.Value || ''} (ID: ${partner.id || ''})` if (partnerName2) { const nameParts = partnerName2.trim().split(' ') contactData.company = { name: partnerName, contactPersons: [{ firstName: nameParts[0] || '', lastName: nameParts.slice(1).join(' ') || partnerName2 }] } } else { const companyIndicators = ['GmbH', 'AG', 'KG', 'OHG', 'Ltd', 'Inc', 'LLC', 'UG', 'e.K.', 'Co.', '&', 'mbH'] const isCompany = companyIndicators.some(indicator => partnerName.includes(indicator)) if (isCompany) { contactData.company = { name: partnerName } } else { const nameParts = partnerName.trim().split(' ') if (nameParts.length > 1) { contactData.person = { firstName: nameParts[0], lastName: nameParts.slice(1).join(' ') } } else { contactData.person = { lastName: partnerName } } } } logger?.log(`Contact data: ${JSON.stringify(contactData)}`) const result = await lexofficeRequest('/v1/contacts', 'POST', contactData, logger) return result } // Upload file to lexoffice as voucher const uploadFileToLexoffice = async (fileBuffer: Buffer, fileName: string, logger?: ReturnType): Promise => { logger?.log(`Uploading file: ${fileName} (${fileBuffer.length} bytes)`) // Use native FormData and Blob for Node.js 18+ const blob = new Blob([fileBuffer], { type: 'application/pdf' }) const formData = new FormData() formData.append('file', blob, fileName) formData.append('type', 'voucher') logger?.log(`FormData created with file blob and type=voucher`) const response = await fetch(`${LEXOFFICE_API_URL}/v1/files`, { method: 'POST', headers: { 'Authorization': `Bearer ${LEXOFFICE_API_KEY}`, 'Accept': 'application/json' }, body: formData }) if (!response.ok) { const errorText = await response.text() logger?.log(`File upload failed (${response.status}): ${errorText}`) throw new Error(`File upload failed (${response.status}): ${errorText}`) } const locationHeader = response.headers.get('Location') let responseData: any = {} try { responseData = await response.json() } catch (e) {} const fileId = responseData.id || locationHeader?.split('/').pop() logger?.log(`File uploaded successfully: ${fileId}`) return { id: fileId, ...responseData } } const handleFunc = async (event: any, authToken: any = null) => { const logger = createLogger() let data: any = {} const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const invoiceIds: number[] = body.ids || [] logger.log(`Starting Lexoffice upload for invoice IDs: ${JSON.stringify(invoiceIds)}`) if (!invoiceIds.length) { return { status: 400, message: 'No invoice IDs provided', logs: logger.getLogs() } } const errors: string[] = [] const skipped: string[] = [] let uploadedCount = 0 for (const invoiceId of invoiceIds) { try { logger.log(`--- Processing invoice ID: ${invoiceId} ---`) // Fetch invoice (no $expand) const invoice: any = await event.context.fetch( `models/c_invoice/${invoiceId}`, 'GET', token, null ) if (!invoice) { const msg = `Invoice ${invoiceId} not found` logger.log(msg) errors.push(msg) continue } logger.log(`Invoice found: ${invoice.DocumentNo} (DocBaseType: ${invoice.DocBaseType?.id || invoice.DocBaseType})`) // Check if already uploaded to Lexoffice if (invoice.isUploadToLexoffice === true || invoice.isUploadToLexoffice === 'Y') { const msg = `Invoice ${invoice.DocumentNo} already uploaded to Lexoffice - skipping` logger.log(msg) skipped.push(msg) continue } // Fetch partner separately const partnerId = invoice.C_BPartner_ID?.id || invoice.C_BPartner_ID if (!partnerId) { const msg = `Invoice ${invoice.DocumentNo}: No business partner found` logger.log(msg) errors.push(msg) continue } logger.log(`Fetching partner ID: ${partnerId}`) const partner: any = await event.context.fetch( `models/c_bpartner/${partnerId}`, 'GET', token, null ) if (!partner) { const msg = `Invoice ${invoice.DocumentNo}: Could not fetch partner` logger.log(msg) errors.push(msg) continue } logger.log(`Partner: ${partner.Name} (Value: ${partner.Value}, IsCustomer: ${partner.IsCustomer}, IsVendor: ${partner.IsVendor})`) // Step 1: Find or create contact in Lexoffice let lexofficeContactId: string | undefined = undefined // First check if partner already has lexware_contact_uuid saved if (partner.lexware_contact_uuid) { lexofficeContactId = partner.lexware_contact_uuid logger.log(`Partner already has lexware_contact_uuid: ${lexofficeContactId}`) } else { // Try to find existing contact by name in Lexoffice const existingContact = await findContact(partner.Name, logger) if (existingContact) { lexofficeContactId = existingContact.id } else { // Create new contact try { const newContact = await createLexofficeContact(partner, logger) lexofficeContactId = newContact.id } catch (contactErr: any) { logger.log(`Could not create contact for ${partner.Name}: ${contactErr.message}`) } } // Save lexoffice contact UUID back to C_BPartner if (lexofficeContactId) { try { logger.log(`Saving lexware_contact_uuid "${lexofficeContactId}" to c_bpartner ${partnerId}`) await event.context.fetch( `models/c_bpartner/${partnerId}`, 'PUT', token, { lexware_contact_uuid: lexofficeContactId } ) logger.log(`Successfully saved lexware_contact_uuid to c_bpartner`) } catch (saveErr: any) { logger.log(`Could not save lexware_contact_uuid to c_bpartner: ${saveErr.message}`) } } } // Rate limiting await new Promise(resolve => setTimeout(resolve, 500)) // Step 2: Fetch invoice PDF and upload to Lexoffice try { logger.log(`Fetching PDF for invoice ${invoiceId}`) const pdfResponse: any = await event.context.fetch( `models/c_invoice/${invoiceId}/print?$report_type=PDF`, 'GET', token, null ) if (pdfResponse?.reportFile) { const pdfBuffer = Buffer.from(pdfResponse.reportFile, 'base64') logger.log(`PDF generated: ${pdfBuffer.length} bytes`) // Determine type from DocBaseType const docBaseType = invoice.DocBaseType?.id || invoice.DocBaseType?.identifier || invoice.DocBaseType || '' const isOutgoing = docBaseType === 'ARI' || docBaseType === 'ARC' const typePrefix = isOutgoing ? 'AR' : 'AP' logger.log(`DocBaseType: ${docBaseType}, Type: ${typePrefix}`) const uploadResult = await uploadFileToLexoffice( pdfBuffer, `${typePrefix}_${invoice.DocumentNo}_${partner.Name || 'Unknown'}.pdf`, logger ) // Step 3: Mark invoice as uploaded in iDempiere try { const uploadDate = new Date().toISOString() logger.log(`Marking invoice ${invoiceId} as uploaded to Lexoffice`) await event.context.fetch( `models/c_invoice/${invoiceId}`, 'PUT', token, { isUploadToLexoffice: true, UploadDateLexoffice: uploadDate } ) logger.log(`Successfully marked invoice as uploaded`) } catch (markErr: any) { logger.log(`Could not mark invoice as uploaded: ${markErr.message}`) } logger.log(`SUCCESS: Invoice ${invoice.DocumentNo} uploaded to Lexoffice with file ID: ${uploadResult.id}`) uploadedCount++ } else { const msg = `Invoice ${invoice.DocumentNo}: Could not generate PDF (reportFile is missing)` logger.log(msg) errors.push(msg) } } catch (pdfErr: any) { const msg = `Invoice ${invoice.DocumentNo}: ${pdfErr.message || 'Failed to upload'}` logger.log(msg) errors.push(msg) } // Rate limiting await new Promise(resolve => setTimeout(resolve, 500)) } catch (err: any) { const msg = `Error processing invoice ${invoiceId}: ${err.message || 'Unknown error'}` logger.log(msg) errors.push(msg) } } logger.log(`=== SUMMARY: Uploaded ${uploadedCount} invoice(s), ${skipped.length} skipped, ${errors.length} error(s) ===`) // Output all logs to console for debugging console.log('\n========== LEXOFFICE UPLOAD LOGS ==========') logger.getLogs().forEach(log => console.log(log)) console.log('============================================\n') if (uploadedCount > 0) { data = { status: 200, message: `Successfully uploaded ${uploadedCount} invoice(s) to Lexoffice${skipped.length > 0 ? `, ${skipped.length} skipped (already uploaded)` : ''}`, uploadedCount, skippedCount: skipped.length, uploadDate: new Date().toISOString(), errors: errors.length > 0 ? errors : undefined, skipped: skipped.length > 0 ? skipped : undefined, logs: logger.getLogs() } } else if (skipped.length > 0 && errors.length === 0) { data = { status: 200, message: `All ${skipped.length} invoice(s) were already uploaded to Lexoffice`, uploadedCount: 0, skippedCount: skipped.length, skipped, logs: logger.getLogs() } } else { data = { status: 400, message: 'No invoices were uploaded to Lexoffice', errors, skipped: skipped.length > 0 ? skipped : undefined, logs: logger.getLogs() } } return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })