Introduction:
In today's digital business landscape, effective document management is crucial for maintaining operational efficiency and ensuring seamless workflows. Salesforce, as a leading CRM platform, offers robust functionalities for managing documents, including the ability to generate PDFs dynamically using Apex, its proprietary programming language. However, despite its capabilities, Salesforce lacks the ability to merge multiple PDF documents without using an Salesforce app (i.e. using only Apex), presenting a challenge for organizations that require this functionality within their Salesforce environment.
The Problem:
Consider a scenario where a company uses Salesforce to manage quotes and invoices. Each quote is generated as a PDF document, often containing detailed information specific to a customer's request. However, there may be instances where additional documents, such as terms and conditions or product brochures, need to be included alongside the quote PDF before it's finalized and sent to the customer. Salesforce's inability to merge PDF documents directly within its platform creates a bottleneck in the document generation and delivery process, leading to manual intervention and potential errors.
To address this challenge, we need a solution that seamlessly integrates with Salesforce, allowing for the merging of PDF documents generated within the platform. By leveraging external services and technologies, such as Node.js, we can extend Salesforce's capabilities and streamline document management workflows, enhancing productivity and ensuring accuracy in document generation and delivery processes.
Apex Code for PDF Rendering:
@future(callout=true)
static void createMergedPdfDocumentForQuote(Id quoteId, String pdfName) {
// Retrieve quote details
Quote quote = [SELECT Id, OpportunityId, Name FROM Quote WHERE Id =: quoteId];
Id oppId = quote.OpportunityId;
// Prepare HTTP request to send PDF data to Node.js server
HttpRequest req = new HttpRequest();
String endpoint = [SELECT IsSandbox FROM Organization].IsSandbox ?
'myTestUrl/mergeQuotePdf' : 'myProdUrl/mergeQuotePdf';
req.setEndpoint(endpoint);
req.setMethod('POST');
// Generate PDF using Visualforce page
Blob quotePdfBlob;
if (!Test.isRunningTest()) {
PageReference pr = Page.MyVfPdfPage;
pr.getParameters().put('id', quote.Id);
Blob b = pr.getContentAsPdf();
quotePdfBlob = b;
}
// Prepare request body with PDF data
pdfName = pdfName.replace('\\','');
String requestBody = '{ "data": { "pdfName": "' + pdfName + '", "quotePdf": "' +
EncodingUtil.base64Encode(quotePdfBlob) + '" }}';
req.setBody(requestBody);
req.setTimeout(100000);
req.setHeader('Content-Type', 'application/json');
// Send HTTP request to Node.js server
Http http = new Http();
HttpResponse res = http.send(req);
// Handle response
if (res.getStatusCode() != 200) {
sendErrorEmail(res.getBody());
}
}
This Apex method, createMergedPdfDocumentForQuote, is decorated with the @future(callout=true) annotation, allowing it to execute asynchronously and perform callouts to external services. When invoked, it queries a Quote record and retrieves its details, including the associated Opportunity. Then, it constructs an HTTP request to send the PDF data to a Node.js server for merging.
The PDF is generated dynamically using a Visualforce page named "MyVfPdfPage". This page likely contains the layout and content structure for the quote invoice. If the method is not running in a test context, the PDF content is retrieved using getContentAsPdf() method from the Visualforce page.
The PDF data and metadata (such as the PDF name) are formatted into a JSON string and included in the request body. The HTTP request is then sent to the Node.js server's designated endpoint.
Any non-200 status code in the response triggers an error handling mechanism, which might involve sending an email notification to administrators for further investigation.
Node.js Code for PDF Merging:
import PDFMerger from "pdf-merger-js";
export const generateMergedQuotePdfForQuote = functions.https.onCall(async (data, context) => {
// Login to Salesforce and retrieve access token
const accessToken = await loginToSalesforceAndGetToken();
// Write quote PDF to local file system
fs.writeFileSync(`/tmp/quotepdf.pdf`, Buffer.from(data.quotePdf, 'base64'), { encoding: 'binary' });
// Retrieve another document from Salesforce to merge with quote PDF
await new Promise<void>(resolve => request.get(
`${salesForceConfig.apiendpoint}/services/data/v57.0/sobjects/Attachment/myAttachment/body`,
{
'auth': {
'bearer': accessToken
},
json: true,
encoding: 'binary'
},
function (error: any, response: any, body: any) {
fs.writeFileSync(`/tmp/myAttachment.pdf`, body, { encoding: 'binary' });
resolve();
}
));
// Merge PDFs using pdf-merger-js library
const merger = new PDFMerger();
await merger.add(`/tmp/quotepdf.pdf`);
await merger.add(`/tmp/myAttachment.pdf`);
await merger.save('/tmp/merged.pdf');
// Export merged PDF as nodejs Buffer
const mergedPdfBuffer = await merger.saveAsBuffer();
await fs.writeFileSync('/tmp/merged.pdf', mergedPdfBuffer);
// Read merged PDF content
const contents = fs.readFileSync('/tmp/merged.pdf', { encoding: 'base64' });
// Upload merged document back to Salesforce as Attachment
await new Promise<void>(resolve => request.post(
`${salesForceConfig.apiendpoint}/services/data/v57.0/sobjects/Attachment`,
{
'auth': {
'bearer': accessToken
},
headers: {
"Content-Type": "application/json",
},
json: true,
body: {
name: `${data.quoteId} Quote`,
parentId: data.quoteId,
ContentType: 'application/pdf',
body: contents
}
},
function (error: any, response: any, body: any) {
resolve();
}
));
return Promise.resolve();
});
This Node.js function, generateMergedQuotePdfForQuote, is deployed as a Firebase Cloud Function and is invoked via HTTPS calls, typically from the Salesforce Apex method described earlier.
Upon invocation, it receives the PDF data sent from Salesforce, which represents the quote PDF. It also retrieves another PDF document from Salesforce (likely an attachment associated with the quote).
Once both PDF documents are obtained, the function uses the pdf-merger-js library to merge them into a single PDF file. After merging, the resulting PDF is stored temporarily on the server's file system.
The merged PDF content is then read, encoded as base64, and uploaded back to Salesforce as an Attachment using the Salesforce REST API. This completes the cycle of merging and uploading the merged PDF document.
Conclusion:
Through this detailed breakdown, we've gained insights into how the integration of Apex and Node.js enables seamless PDF merging in Salesforce. This solution empowers businesses to efficiently manage document workflows, overcoming the limitations posed by Salesforce's native capabilities. By understanding the intricacies of each component, we can appreciate the elegance and effectiveness of this integration approach.
Comments