From 40798dfe636ec82b73cb5850fd892d6ffd61d29f Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Fri, 13 Mar 2026 00:05:02 -0400 Subject: [PATCH] feat(sms): draft envelope + Docusign Sender View for recipients without email When the primary recipient (Docusign Recipient #1) has no email address, the envelope is created in draft status and the sender completes delivery manually in the Docusign Sender View web console where they can configure SMS delivery. Apex changes: - DocusignCompositeEnvelopeBuilder: new DRAFT_PLACEHOLDER_EMAIL constant; when requiresDraftMode=true calls sendEnvelope(envelope, false) to create a draft, then builds and returns a Sender View URL via buildSenderViewUrl(); buildRecipient accepts allowPlaceholderEmail flag and substitutes placeholder when email is blank - DocusignEnvelopeRequest: new requiresDraftMode Boolean InvocableVariable - DocusignEnvelopeResult: new senderViewUrl String InvocableVariable Flow (V3) changes: - Get_Records now fetches Docusign_Recipient_1__c - New Get_Recipient_Contact lookup queries Contact.Email and Name - New Is_Recipient_Email_Blank decision routes to Set_Draft_Mode when blank - New Set_Draft_Mode assignment sets requiresDraftMode=true - New No_Email_Warning_Screen explains the draft process and required steps - requiresDraftMode passed as input; senderViewUrl captured as output - New Is_Draft_Envelope decision routes to Sender_View_Screen or Success_Screen - New Sender_View_Screen shows clickable link, placeholder email, and step-by-step instructions for configuring SMS delivery in the Docusign web console --- .../DocusignCompositeEnvelopeBuilder.cls | 113 ++- .../classes/DocusignEnvelopeRequest.cls | 7 + .../classes/DocusignEnvelopeResult.cls | 6 + ...cusign_Envelope_Templates_V4.flow-meta.xml | 868 ++++++++++++++++++ 4 files changed, 982 insertions(+), 12 deletions(-) create mode 100644 composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls index 95d00d5..77fc72e 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls @@ -28,6 +28,17 @@ global with sharing class DocusignCompositeEnvelopeBuilder { // ============================================================ @TestVisible private static final String MULTI_COPY_TEMPLATE_NAME = 'Authorization to Release Information'; + + // ============================================================ + // DRAFT MODE / SENDER VIEW: When the primary recipient has no email + // and requiresDraftMode=true, the envelope is created in draft status. + // Docusign requires an email address on every recipient even in draft + // mode, so this placeholder satisfies the API requirement. + // The sender will replace this with the real SMS recipient details + // manually in the Docusign Sender View before sending. + // ============================================================ + @TestVisible + private static final String DRAFT_PLACEHOLDER_EMAIL = 'placeholder_email@docusign.com'; @InvocableMethod( label='Send Composite Docusign Envelope' @@ -188,7 +199,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder { } // Resolve recipients from Client_Case__c lookup fields - List recipients = resolveRecipients(req.recordId); + List recipients = resolveRecipients(req.recordId, draftMode); myEnvelope = myEnvelope.withRecipients(recipients); // Set envelope subject to combined display names (deduplicated, with copy counts). @@ -222,15 +233,29 @@ global with sharing class DocusignCompositeEnvelopeBuilder { String envelopeBody = bodyParts.isEmpty() ? '' : String.join(bodyParts, '\n\n'); myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody); - // Send the envelope - myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true); + // Send the envelope — or create it as a draft when requiresDraftMode is true. + // + // Draft mode is used when the primary recipient has no email address and will + // need SMS delivery configured manually in the Docusign Sender View. + // dfsle.EnvelopeService.sendEnvelope(envelope, false) creates the envelope in + // "created" (draft) status without sending it. We then build the Sender View + // URL so the flow can present a direct link to the Docusign web console. + Boolean draftMode = (req.requiresDraftMode == true); + myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, !draftMode); - // Success result.envelopeId = String.valueOf(myEnvelope.docuSignId); result.success = true; result.errorMessage = null; - - logResult(sortedTemplateIds.size(), result.envelopeId, 'Success (' + String.join(displayNames, ', ') + ')', null); + + if (draftMode) { + result.senderViewUrl = buildSenderViewUrl(result.envelopeId); + logResult(sortedTemplateIds.size(), result.envelopeId, + 'Draft created — Sender View required (' + String.join(displayNames, ', ') + ')', null); + } else { + result.senderViewUrl = null; + logResult(sortedTemplateIds.size(), result.envelopeId, + 'Success (' + String.join(displayNames, ', ') + ')', null); + } } catch (Exception e) { result.success = false; @@ -256,9 +281,12 @@ global with sharing class DocusignCompositeEnvelopeBuilder { * @description Resolves recipients from Client_Case__c lookup fields. * Queries the case record and related contacts to get name/email. * @param recordId The Client_Case__c record ID + * @param draftMode When true, a missing email on the primary recipient is allowed — + * DRAFT_PLACEHOLDER_EMAIL is substituted so the dfsle call succeeds. + * The sender will update the recipient in the Docusign Sender View. * @return List of dfsle.Recipient objects with role mappings */ - private static List resolveRecipients(String recordId) { + private static List resolveRecipients(String recordId, Boolean draftMode) { // Query the Client_Case__c record with recipient lookup fields // NOTE: Adjust field API names if they differ in your org String query = 'SELECT Id, ' @@ -274,13 +302,13 @@ global with sharing class DocusignCompositeEnvelopeBuilder { // Recipient 1: Service Coordinator Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR); if (serviceCoordinatorId != null) { - recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId)); + recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId, false)); } // Recipient 2: Docusign Recipient #1 Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT); if (docusignRecipientId != null) { - recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId)); + recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId, draftMode)); } if (recipients.isEmpty()) { @@ -297,9 +325,12 @@ global with sharing class DocusignCompositeEnvelopeBuilder { * @param roleName The Docusign template role name * @param routingOrder Signing order * @param sourceRecordId The source Client_Case__c record ID + * @param allowPlaceholderEmail When true and the recipient has no email, substitutes + * DRAFT_PLACEHOLDER_EMAIL so the dfsle call succeeds. + * Only set true for the primary recipient in draft mode. * @return dfsle.Recipient configured for the role */ - private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId) { + private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId, Boolean allowPlaceholderEmail) { // Determine if this is a Contact or User String objectType = recipientId.getSObjectType().getDescribe().getName(); @@ -320,8 +351,14 @@ global with sharing class DocusignCompositeEnvelopeBuilder { } if (String.isBlank(recipientEmail)) { - throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). ' - + 'Please ensure the recipient has a valid email address.'); + if (allowPlaceholderEmail == true) { + // Draft mode — substitute placeholder so the envelope can be created. + // The sender will update this recipient's details in the Docusign Sender View. + recipientEmail = DRAFT_PLACEHOLDER_EMAIL; + } else { + throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). ' + + 'Please ensure the recipient has a valid email address.'); + } } return dfsle.Recipient.fromSource( @@ -342,6 +379,58 @@ global with sharing class DocusignCompositeEnvelopeBuilder { System.debug(LoggingLevel.ERROR, 'Error: ' + errorMessage); } } + + /** + * @description Builds the Docusign Sender View URL for a draft envelope. + * The URL opens the envelope in the Docusign web console so the sender + * can configure SMS delivery and send it manually. + * + * URL format: + * Production: https://app.docusign.com/documents/details/{envelopeId} + * Demo/sandbox: https://app.docusign.net/documents/details/{envelopeId} + * + * Base_URL__c on the Docusign_Configuration__c custom setting stores the + * callout base URL (e.g. https://demo.docusign.net/restapi or + * https://na3.docusign.net/restapi). We derive the web console host from + * that so the link always points to the correct environment. + * + * @param envelopeId The Docusign envelope ID (GUID) of the draft envelope + * @return Full Sender View URL string + */ + @TestVisible + private static String buildSenderViewUrl(String envelopeId) { + // Determine the web console host from Base_URL__c. + // Base_URL__c looks like: https://demo.docusign.net/restapi + // or: https://na3.docusign.net/restapi + // We extract the scheme + host and append the documents/details path. + String webHost = 'https://app.docusign.com'; // default: production + try { + Docusign_Configuration__c config = Docusign_Configuration__c.getInstance(); + if (config != null && String.isNotBlank(config.Base_URL__c)) { + String baseUrl = config.Base_URL__c.trim(); + // Strip trailing path components to get just scheme://host + Integer restApiIdx = baseUrl.toLowerCase().indexOf('/restapi'); + if (restApiIdx > 0) { + baseUrl = baseUrl.left(restApiIdx); + } + // Replace API host with web console host: + // demo.docusign.net → app.docusign.net (sandbox/demo) + // *.docusign.net → app.docusign.net (other NA/EU pods) + // app.docusign.com → app.docusign.com (production) + if (baseUrl.containsIgnoreCase('demo.docusign')) { + webHost = 'https://app.docusign.net'; + } else if (baseUrl.containsIgnoreCase('.docusign.net')) { + webHost = 'https://app.docusign.net'; + } else { + webHost = 'https://app.docusign.com'; + } + } + } catch (Exception ex) { + // If config lookup fails, fall back to production URL + System.debug(LoggingLevel.WARN, 'Could not read Base_URL__c for Sender View URL: ' + ex.getMessage()); + } + return webHost + '/documents/details/' + envelopeId; + } /** * @description Strips language suffixes like " - English" or " - Spanish" from template names diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeRequest.cls b/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeRequest.cls index 84c074d..6320dcc 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeRequest.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeRequest.cls @@ -39,4 +39,11 @@ global class DocusignEnvelopeRequest { required=false ) global Integer authReleaseFormCopies; + + @InvocableVariable( + label='Requires Draft Mode' + description='When true, the envelope is created in draft (created) status instead of being sent immediately. Used when the primary recipient has no email address and SMS delivery must be configured manually in the Docusign Sender View. The result will contain a senderViewUrl the user should open to complete sending.' + required=false + ) + global Boolean requiresDraftMode; } diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeResult.cls b/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeResult.cls index 9cd5c53..5e66027 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeResult.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignEnvelopeResult.cls @@ -22,4 +22,10 @@ global class DocusignEnvelopeResult { description='Error message if envelope creation failed' ) global String errorMessage; + + @InvocableVariable( + label='Sender View URL' + description='Docusign Sender View URL. Populated when the envelope was created as a draft because the primary recipient has no email address. The user must open this URL in the Docusign web console to configure SMS delivery and send the envelope manually.' + ) + global String senderViewUrl; } diff --git a/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml b/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml new file mode 100644 index 0000000..e2586a9 --- /dev/null +++ b/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml @@ -0,0 +1,868 @@ + + + + Send_Composite_Envelope + + 182 + 1082 + DocusignCompositeEnvelopeBuilder + apex + + Check_Envelope_Result + + Automatic + + templateIds + + compositeTemplateIds + + + + recordId + + recordId + + + + language + + Get_Records.Docusign_Envelope_Language__c + + + + authReleaseFormCopies + + authReleaseFormCopies + + + + requiresDraftMode + + requiresDraftMode + + + DocusignCompositeEnvelopeBuilder + 0 + + envelopeId + envelopeId + + + envelopeSuccess + success + + + envelopeErrorMessage + errorMessage + + + senderViewUrl + senderViewUrl + + + 60.0 + false + + Add_Template_ID + + 270 + 1106 + + compositeTemplateIds + Add + + Build_Template_ID_Collection.dfsle__DocuSignId__c + + + + Build_Template_ID_Collection + + + + Flag_Auth_Release_Selected + + 270 + 1000 + + authReleaseTemplateSelected + Assign + + true + + + + Scan_For_Auth_Release_Template + + + + Store_Auth_Release_Copies + + 182 + 1160 + + authReleaseFormCopies + Assign + + authReleaseFormCopies_Radio + + + + Build_Template_ID_Collection + + + + Check_Envelope_Result + + 182 + 1190 + + Error_Screen + + Default Outcome + + Envelope_Sent_Successfully + and + + envelopeSuccess + EqualTo + + true + + + + Is_Draft_Envelope + + + + + + Check_Row_Selection + + 380 + 674 + + Row_not_selected + + Default Outcome + + Is_Row_Selected + and + + data.firstSelectedRow.Id + IsNull + + false + + + + Scan_For_Auth_Release_Template + + + + + + Is_Auth_Release_Selected + + 380 + 890 + + Build_Template_ID_Collection + + No - Proceed + + Auth_Release_Template_Found + and + + authReleaseTemplateSelected + EqualTo + + true + + + + Authorization_Copies_Screen + + + + + + Is_Language_Selected + + 611 + 458 + + Language_Not_Added_Screen + + Default Outcome + + Language_Selected + and + + Get_Records.Docusign_Envelope_Language__c + IsNull + + false + + + + Language_Warning_Screen + + + + + + Does_Row_Contain_Auth_Release + + 270 + 890 + + Scan_For_Auth_Release_Template + + No + + Row_Is_Auth_Release_Template + and + + Scan_For_Auth_Release_Template.Name + Contains + + Authorization to Release Information + + + + Flag_Auth_Release_Selected + + + + + + Get_Recipient_Contact + + 611 + 242 + true + + Is_Recipient_Email_Blank + + and + + Id + EqualTo + + Get_Records.Docusign_Recipient_1__c + + + true + Contact + Id + Email + Name + true + + + Is_Recipient_Email_Blank + + 611 + 350 + + Is_Language_Selected + + Has Email - Continue + + Recipient_Has_No_Email + or + + Get_Recipient_Contact.Email + IsNull + + true + + + + Get_Recipient_Contact.Email + EqualTo + + + + + + Set_Draft_Mode + + + + + + Set_Draft_Mode + + 842 + 350 + + requiresDraftMode + Assign + + true + + + + No_Email_Warning_Screen + + + + No_Email_Warning_Screen + + 842 + 458 + false + true + false + + Is_Language_Selected + + + NoEmailWarningText + <p>⚠️ <strong>The primary recipient does not have an email address on file.</strong></p> +<p><br></p> +<p>Because Docusign requires an email address to create an envelope, a <strong>draft envelope</strong> will be created using a placeholder email address (<strong>placeholder_email@docusign.com</strong>). You will then be taken to the Docusign web console to complete sending.</p> +<p><br></p> +<p><strong>Once in the Docusign Sender View you must:</strong></p> +<ol> +<li>Find the recipient <strong>{!Get_Recipient_Contact.Name}</strong> (currently showing placeholder_email@docusign.com).</li> +<li>Edit the recipient and <strong>enable SMS delivery</strong> for that recipient.</li> +<li>Enter the recipient's <strong>mobile phone number</strong> for SMS delivery.</li> +<li>Click <strong>Send</strong> to dispatch the envelope.</li> +</ol> +<p><br></p> +<p>Click <strong>Next</strong> to proceed to template selection.</p> + DisplayText + + + top + + + 12 + + + + Next + true + true + + + Is_Draft_Envelope + + 182 + 1298 + + Success_Screen + + No — Sent Normally + + Draft_Envelope_Created + and + + senderViewUrl + IsNull + + false + + + + Sender_View_Screen + + + + + + Sender_View_Screen + + 314 + 1406 + false + true + false + + SenderViewInstructions + <p>✅ <strong>Draft envelope created successfully.</strong></p> +<p><strong>Envelope ID:</strong> {!envelopeId}</p> +<p><br></p> +<p>The envelope has been saved as a <strong>draft</strong> because the primary recipient <strong>{!Get_Recipient_Contact.Name}</strong> has no email address. You must complete sending in the Docusign web console.</p> +<p><br></p> +<p><strong>Steps to complete:</strong></p> +<ol> +<li>Click the link below to open the envelope in the <strong>Docusign Sender View</strong>.</li> +<li>Find the recipient showing <strong>placeholder_email@docusign.com</strong>.</li> +<li>Edit that recipient — enable <strong>SMS delivery</strong> and enter their mobile phone number.</li> +<li>Click <strong>Send</strong> in Docusign to dispatch the envelope.</li> +</ol> +<p><br></p> +<p>🔗 <a href="{!senderViewUrl}" target="_blank"><strong>Open Envelope in Docusign</strong></a></p> +<p><br></p> +<p><em>If the link does not open, copy and paste this URL into your browser:</em></p> +<p>{!senderViewUrl}</p> + DisplayText + + + top + + + 12 + + + + true + false + + Default + Docusign Envelope Templates V4 {!$Flow.CurrentDateTime} + + + Build_Template_ID_Collection + + 182 + 1214 + data.selectedRows + Asc + + Add_Template_ID + + + Send_Composite_Envelope + + + + Scan_For_Auth_Release_Template + + 380 + 782 + data.selectedRows + Asc + + Does_Row_Contain_Auth_Release + + + Is_Auth_Release_Selected + + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + Flow + + DocuSign_Envelope_Templates + + 380 + 458 + false + + Envelope_template_records + + and + + Envelope_Template_Language__c + EqualTo + + Get_Records.Docusign_Envelope_Language__c + + + + Short_Name__c + IsNull + + false + + + false + dfsle__EnvelopeConfiguration__c + Id + Name + dfsle__DocuSignId__c + Name + Asc + true + + + Get_Records + + 611 + 134 + false + + Get_Recipient_Contact + + and + + Id + EqualTo + + recordId + + + true + Client_Case__c + Id + Docusign_Envelope_Language__c + Docusign_Recipient_1__c + true + + + Envelope_template_records + + 380 + 566 + true + true + false + Back + + Check_Row_Selection + + + data + + T + dfsle__EnvelopeConfiguration__c + + flowruntime:datatable + ComponentInstance + + label + + Select Templates for Composite Envelope + + + + selectionMode + + MULTI_SELECT + + + + minRowSelection + + 0.0 + + + + tableData + + DocuSign_Envelope_Templates + + + + columns + + [{"apiName":"Name","guid":"column-6d57","editable":false,"hasCustomHeaderLabel":true,"customHeaderLabel":"Envelope Template Name","wrapText":true,"order":0,"label":"Name","type":"text"}] + + + UseStoredValues + true + true + + + top + + + 12 + + + + Send + true + true + + + Authorization_Copies_Screen + + 182 + 1106 + true + true + false + Back + + Store_Auth_Release_Copies + + + AuthCopiesHeader + <p>The <strong>Authorization to Release Information</strong> form was selected.</p><p>How many copies of this form should be included in the envelope?</p> + DisplayText + + + top + + + 12 + + + + + authReleaseFormCopies_Radio + AuthCopies_1 + AuthCopies_2 + AuthCopies_3 + Number + AuthCopies_1 + Number of Copies + RadioButtons + true + 0 + + + top + + + 12 + + + + Next + true + true + + + Error_Screen + + 314 + 1298 + true + true + false + Back + + ErrorDisplayMessage + <p><span style="font-size: 16px; color: rgb(255, 0, 0);">❌ Failed to send composite envelope.</span></p><p><br></p><p><strong>Error:</strong> {!envelopeErrorMessage}</p><p><br></p><p>Please try again or contact your administrator.</p> + DisplayText + + + top + + + 12 + + + + true + false + + + Language_Not_Added_Screen + + 842 + 350 + false + true + false + + LanguageNotSelected + <p>The <strong>DocuSign Envelope Language</strong> is not populated on the record. Please add the language first and then proceed.</p> + DisplayText + + + top + + + 12 + + + + true + true + + + Language_Warning_Screen + + 380 + 350 + false + true + false + + DocuSign_Envelope_Templates + + + LangWarningText + <p>The current selected language is <strong>{!Get_Records.Docusign_Envelope_Language__c}. </strong>On the next screen you will be able to see form names of {!Get_Records.Docusign_Envelope_Language__c} language only. If you want to switch the language, please go back to record and select another language form <strong>DocuSign Envelope Language</strong>.</p> + DisplayText + + + top + + + 12 + + + + Next + true + true + + + Row_not_selected + + 578 + 782 + true + true + false + Back + + ErrorMessage + <p><strong style="background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);"><em>You have not selected any of the forms. Please go back and select the form first and then proceed.</em></strong></p> + DisplayText + + + top + + + 12 + + + + true + false + + + Success_Screen + + 50 + 1298 + false + true + false + + SuccessMessage + <p><span style="font-size: 16px; color: rgb(0, 128, 0);">✅ Composite envelope sent successfully!</span></p><p><br></p><p><strong>Envelope ID:</strong> {!envelopeId}</p><p><strong>Templates combined:</strong> All selected templates were merged into a single envelope.</p> + DisplayText + + + top + + + 12 + + + + true + false + + + 485 + 0 + + Get_Records + + + Draft + + compositeTemplateIds + String + true + false + false + + + envelopeErrorMessage + String + false + false + false + + + envelopeId + String + false + false + false + + + envelopeSuccess + Boolean + false + false + false + + + recordId + String + false + true + false + + + authReleaseFormCopies + Number + false + false + false + 0 + + 1.0 + + + + authReleaseTemplateSelected + Boolean + false + false + false + + false + + + + requiresDraftMode + Boolean + false + false + false + + false + + + + senderViewUrl + String + false + false + false + + + AuthCopies_1 + 1 copy + Number + + 1.0 + + + + AuthCopies_2 + 2 copies + Number + + 2.0 + + + + AuthCopies_3 + 3 copies + Number + + 3.0 + + +