287 lines
11 KiB
JavaScript
287 lines
11 KiB
JavaScript
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();
|
|
// excel1 => /Users/ddatinguinoo/Downloads/Estratto IPP 5 Sic.xlsx
|
|
// excel2 => /Users/ddatinguinoo/Downloads/Estratto Haccp IPP 5 (1).xlsx
|
|
// excel3 di prova => /Users/ddatinguinoo/Downloads/File di prova.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.setProductionBaseUrl(); // 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, isDryRun = false) {
|
|
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 = [];
|
|
let matchAccount = null;
|
|
|
|
const userID = result.text.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i);
|
|
const codiceFiscale = userID ? userID[1].trim() : null;
|
|
if (codiceFiscale) {
|
|
const cfSoql = `SELECT Id, Name, CF__c FROM Account WHERE CF__c='${codiceFiscale }' ORDER BY CreatedDate ASC LIMIT 1`;
|
|
const cfResult = await sfdc.query(cfSoql);
|
|
|
|
if (cfResult && cfResult.records && cfResult.records.length > 0) {
|
|
matchAccount = cfResult.records[0];
|
|
}
|
|
}
|
|
|
|
// 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(' ');
|
|
|
|
if (fullName !== reversedName) {
|
|
conditions.push(`Name='${ escapeSoql(reversedName) }'`);
|
|
}
|
|
}
|
|
else {
|
|
conditions.push(`Name='${ escapeSoql(rawName) }'`);
|
|
}
|
|
|
|
const nameSoql = `SELECT Id, Name, CF__c FROM Account WHERE (${conditions.join(' OR ')}) ORDER BY CF__c DESC NULLS LAST, CreatedDate ASC LIMIT 1`;
|
|
const nameResult = await sfdc.query(nameSoql);
|
|
|
|
if (nameResult && nameResult.records && nameResult.records.length > 0) {
|
|
matchAccount = nameResult.records[0];
|
|
}
|
|
}
|
|
|
|
if (matchAccount) { // if we find something
|
|
console.log('--- Match found! ---');
|
|
if (isDryRun) {
|
|
console.log('DRY RUN: non creato documento');
|
|
}
|
|
else {
|
|
const nowIsoString = new Date().toISOString(); // current timestamp in UTC
|
|
const newDocument = await sfdc.create('Documento__c', {
|
|
Account__c: `${ matchAccount.Id }`,
|
|
Name: 'Attestato di Formazione',
|
|
Data_Caricamento__c: `${ nowIsoString }`,
|
|
Tipo_Documento__c: "Dossier di Tirocinio",
|
|
Stato__c: 'Valido'
|
|
});
|
|
|
|
// this is the query that is needed to extract the Id of the client and assign it
|
|
// to FirstPublishLocationId field
|
|
const soql2 = `SELECT Account__c FROM Documento__c WHERE Id='${ newDocument.id }'`;
|
|
const docQuery = await sfdc.query(soql2);
|
|
contentVersion = await sfdc.create('ContentVersion', {
|
|
Title: `${ matchAccount.Name }_Document.pdf`,
|
|
PathOnClient: `${ matchAccount.Name }_Document.pdf`,
|
|
VersionData: base64Pdf, // Must be Base64 string
|
|
FirstPublishLocationId: `${ docQuery.records[0].Account__c }`
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
console.log('Account non trovato su Salesforce!');
|
|
}
|
|
console.log(`Name: ${ matchAccount.Name }`);
|
|
console.log(`Name: ${ matchAccount.Id }`);
|
|
}
|
|
finally {
|
|
if (parser) {
|
|
await parser.destroy();
|
|
}
|
|
}
|
|
}
|
|
console.log(`PDFs successfully attached!`);
|
|
console.log('Dry run completed!');
|
|
|
|
} catch (error) {
|
|
console.error(`Error uploading PDF: `, 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
|
|
|
|
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; // since it counts the row that represents the header, I need to subtract -1
|
|
// to get the actual number of rows that contains the data
|
|
|
|
return numRows;
|
|
}
|
|
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() {
|
|
const isMyFlag = process.argv.includes('--myFlag') || process.env.MY_FLAG === 'true';
|
|
const isDryRun = process.argv.includes('--dryRun') || process.env.DRY_RUN === 'true';
|
|
|
|
if (isMyFlag) {
|
|
console.log(`--- Test di attachPdfToAccount ${isDryRun ? '(MODALITÀ DRY RUN)' : ''} --- \n`);
|
|
|
|
let excelFile = process.argv[2];
|
|
let pdfFile = process.argv[3];
|
|
|
|
if (!pdfFile || !excelFile) {
|
|
console.error('Errore: Fornisci il percorso di un file PDF per il test.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const tempFilePath = [];
|
|
|
|
try {
|
|
let resultExcel = await splitExcelFile(excelFile);
|
|
let resultPdf = await splitPDF(resultExcel, pdfFile, tempFilePath);
|
|
|
|
await initializeSalesforceConnection();
|
|
await attachPdfToAccount(sfdc, resultPdf, isDryRun);
|
|
|
|
} catch (err) {
|
|
console.error('An error occurred in test mode: ', err);
|
|
} finally {
|
|
cleanUp(tempFilePath);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
main(); |