const ExcelJS = require('exceljs'); const { PDFParse } = require('pdf-parse'); const { PDFDocument } = require('pdf-lib'); const fs = require('fs'); const os = require('os'); const path = require('path'); const sfdc = require('./sfdc2'); require('dotenv').config(); let excelFile = process.argv[2]; let pdfFile = process.argv[3]; // excel1 => /Users/ddatinguinoo/Downloads/Estratto IPP 5 Sic.xlsx // excel2 => /Users/ddatinguinoo/Downloads/Estratto Haccp IPP 5 (1).xlsx // pdf1 => /Users/ddatinguinoo/Downloads/G-S IPP 5.pdf // pdf2 => /Users/ddatinguinoo/Downloads/H IPP 5.pdf const authConfig = { sfdcTokenFile: process.env.SFDC_TOKEN_FILE, // Path to store/retrieve OAuth tokens sfdcClientId: process.env.SFDC_CLIENT_ID, sfdcClientSecret: process.env.SFDC_CLIENT_SECRET }; async function initializeSalesforceConnection() { // sfdc.setProductionBaseUrl(); // or sfdc.setSandboxBaseUrl(); sfdc.setSandboxBaseUrl(); // Example: using sandbox sfdc.setTokenFile(authConfig.sfdcTokenFile); sfdc.setClientId(authConfig.sfdcClientId); sfdc.setClientSecret(authConfig.sfdcClientSecret); // Try to load existing token or initiate OAuth flow if (!await sfdc.getSalesforceToken()) { console.log('No token found. Initiating OAuth2 flow...'); // For command-line scripts where a browser can be opened: await sfdc.initToken((loginUrl) => { console.log(`Please open this URL to authenticate: ${loginUrl}`); }); } else { console.log('Token loaded successfully.'); } // Ensure the access token is valid and refresh if necessary await sfdc.checkToken(); console.log('Salesforce connection is ready.'); } // Escape single quotes for SOQL queries (e.g., D'Angelo -> D\'Angelo) function escapeSoql(str) { return str ? str.replace(/'/g, "\\'") : ''; } // the pdf files contain the identifying ids of the users such as 'Codice Fiscale' (CF) and we use them to attach // the page/pages to the specific account that has that id. If CFs are not available we search based on the // name an lastname of the user async function attachPdfToAccount(sfdc, tempFiles) { let parser; try { let contentVersion; for (let i = 0; i < tempFiles.length; i++) { // 1. Read the local PDF file and convert it to Base64 const pdfBuffer = fs.readFileSync(tempFiles[i]); const base64Pdf = pdfBuffer.toString('base64'); parser = new PDFParse({ data: pdfBuffer }); try { const result = await parser.getText(); let conditions = []; const userID = result.text.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i); const codiceFiscale = userID ? userID[1].trim() : null; if (codiceFiscale) { conditions.push(`CF__c='${ codiceFiscale }'`); } // since the name and lastname of the user are reversed in some files, we need to put it in order // to match the name from the database const nameMatch = result.text.match(/Il\s+Sig\.\/La\s+Sig\.ra[ \t]+([A-Za-zÀ-ÿ']+(?:[ \t]+[A-Za-zÀ-ÿ']+)+)/i); if (nameMatch) { const rawName = nameMatch[1].trim() const parts = rawName.split(/\s+/); if (parts.length >= 2) { const fullName = parts.join(' '); const reversedName = [...parts.slice(1), parts[0]].join(' '); //conditions.push(`Name='${fullName}'`); if (fullName !== reversedName) { conditions.push(`Name='${ escapeSoql(reversedName) }'`); } } conditions.push(`Name='${ escapeSoql(rawName) }'`); } // use the soql command to search in the database // if codice fiscale is not available, I search the user's account based on their name and lastname const soql = `SELECT Id, Name FROM Account WHERE (${conditions.join(' OR ')}) ORDER BY CreatedDate DESC LIMIT 1`; const result_soql = await sfdc.query(soql); if (result_soql && result_soql.records && result_soql.records.length > 0) { // if we find something contentVersion = await sfdc.create('ContentVersion', { Title: `${ result_soql.records[0].Name }_Document.pdf`, PathOnClient: `${ result_soql.records[0].Name }_Document.pdf`, VersionData: base64Pdf, // Must be Base64 string FirstPublishLocationId: result_soql.records[0].Id// Automatically creates the link to the Account! }); } } finally { if (parser) { await parser.destroy(); } } } console.log(`PDFs successfully attached!`); } catch (error) { console.error(`Error uploading PDF: `, error); } } // here we create an Account for each client and upload their pdf file async function uploadUserAccount(data) { await initializeSalesforceConnection(); // now for each client we need to create an Account on Salesforce Sandbox try { for (let i = 0; i < data.length; i++) { const firstName = (data[i].NOME || '').toString().trim(); const lastName = (data[i].COGNOME || '').toString().trim(); let userData = { Name: `${ firstName } ${ lastName }`.trim(), CF__c: data[i].CODICE_FISCALE || null, }; const newAccount = await sfdc.create('Account', userData); // it creates the Account } console.log('Accounts created successfully!'); } catch (error) { console.log('Something went wrong while creating the accounts: ', error); } } // this is where i need to store each page in a temporary pdf file async function processPage(pdfDoc, pageIndex, tempFiles) { try { const newDoc = await PDFDocument.create(); const [ copiedPage ] = await newDoc.copyPages(pdfDoc, [pageIndex]); newDoc.addPage(copiedPage); const pdfBytes = await newDoc.save(); const filepath = path.join(os.tmpdir(), `fileTemp_page_${pageIndex}.pdf`); fs.writeFileSync(filepath, pdfBytes); tempFiles.push(filepath); } catch (error) { console.error('Error in processPage:', error); } } async function splitExcelFile(inputFile) { let numRows; // this parameter is needed when we need to analyze the pdf file in case it has duplicate pages const columnNames = []; // contains the names of the columns (header names) const data = []; // contains the info of all the clients try { console.log(`Loading excel file: ${ inputFile }\n`); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(inputFile); const worksheet = workbook.getWorksheet(1); const headerRow = worksheet.getRow(1).actualCellCount; //number of columns of the table numRows = (worksheet.actualRowCount) - 1; worksheet.getRow(1) // to convert every title of the first row to upper case .eachCell((col, col_num) => { if (typeof col.value === 'string') { col.value = col.value.toUpperCase().trim(); } }) // convert every letter of CF to uppercase and store the CFs in the array cf if (worksheet.getRow(1).getCell(6).value.trim() === 'CODICE FISCALE') { worksheet.getColumn(6) .eachCell((row, row_num) => { row.value = (row.value).toString().toUpperCase(); }) } worksheet.getColumn(8) // of the first file .eachCell((cell, rownumber) => { if (typeof cell.value === 'string') { cell.value = (cell.value).replace(/\b\w/g, char => char.toUpperCase()); /* \b finds the start of a word (the boundary of a word) \w targets the first letter following that boundary /g (global flag) ensures it updates every word */ } }) // to store the names of the columns in columnNames worksheet.getRow(1).eachCell({ includeEmpty: true }, (cell, cell_num) => { columnNames.push(cell.value); }) // now we need to store the data of each user for (let i = 2; i <= numRows + 1; i++) { let dataUser = {}; worksheet.getRow(i) .eachCell({ includeEmpty: true }, (cell, cell_num) => { const rawColumnName = columnNames[cell_num - 1] || `column_${cell_num}`; // 1. Trim whitespace and replace spaces with underscores const cleanKey = rawColumnName .toString() .trim() .replace(/\s+/g, '_'); // \s => matches any whitespaces // + => groups multiple consecutive spaces together into a // single match so you don't end up with multiple underscores. dataUser[cleanKey] = cell.value; }) data.push(dataUser); } return { numRows, data }; } catch (error) { console.error('Error processing the excel file: ', error); } } const getCodiceFiscale = (pageText) => { if (!pageText) return null; const match = pageText.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i); return match ? match[1].toUpperCase() : null; // match[1] is the captured code }; async function splitPDF(numRows, inputFile, tempFiles) { console.log(`Loading pdf file: ${ inputFile }...\n`); let parser; try { // 1. we use pdfParse to read and extract the content of a pdf file const existingPdfBytes = fs.readFileSync(inputFile); parser = new PDFParse({ data: existingPdfBytes }); const result = await parser.getText(); const pagesNumber = result.pages.length; //pages number of the pdf file // 2. we use pdf-lib to create and edit a pdf file const srcPdf = await PDFDocument.load(existingPdfBytes); // if there are duplicates, I only have to print one copy if (pagesNumber > numRows) { // comparison between the number of pages of the pdf file and those of the excel file let index = 1; for (let i = 0; i < pagesNumber; i++) { // we use optional chaining ?. so that when we encounter null or undefined values, the code doesn't crash const prevCF = getCodiceFiscale(result.pages[i + 1]?.text); const currentCF = getCodiceFiscale(result.pages[i].text); if (prevCF && prevCF === currentCF) { continue; } await processPage(srcPdf, i, tempFiles); index++; } } else { for (let i = 0; i < pagesNumber; i++) { await processPage(srcPdf, i, tempFiles); // Pass 0-based index i } } return tempFiles; } catch (error) { console.error(`Error processing the pdf file: `, error); } finally { if (parser) { await parser.destroy(); } } } // cleans up temporary files from OS temp directory function cleanUp(tempFiles) { tempFiles.forEach((file) => { try { if (fs.existsSync(file)) { fs.unlinkSync(file); } } catch (err) { console.error(`Failed in deleting the temporary file ${ file }: `, err); } }) } async function main() { if (!excelFile || !pdfFile) { console.error('Error: Please provide both Excel and PDF paths.\nUsage: node script.js '); process.exit(1); } const tempFilePath = []; // contains the temporary files let resultExcel, resultPdf; try { resultExcel = await splitExcelFile(excelFile); resultPdf = await splitPDF(resultExcel.numRows, pdfFile, tempFilePath); await initializeSalesforceConnection(); await attachPdfToAccount(sfdc, resultPdf); } catch (err) { console.error('An error occurred in main: ', err); } finally { cleanUp(tempFilePath); } } main();