feat(esignature): add EnvelopeCreateResult class and createEnvelope() method

Adds EnvelopeCreateResult inner DTO and createEnvelope(Id, String, String)
to DocusignESignatureService. Option A (template-based): POSTs to
/v2.1/accounts/{accountId}/envelopes with templateId, templateRoles
(Signer), and status=sent. Guards blank templateId and blank appraiser
email; catches all exceptions and wraps in result. Named credential
sourced from CLM_Account_Setting__mdt per ADR-002.

Agent: claude-sonnet-4-6
Tests: 8/8 passing | N/A (no new test methods — Task 3 covers test coverage)
Tests-Added: 0
TypeScript: N/A (Apex project)
This commit is contained in:
paulh 2026-04-09 22:17:08 -04:00
parent e4b7456dc1
commit c3421e858f
2 changed files with 81 additions and 1 deletions

View File

@ -10,7 +10,7 @@
- [x] **Task 1 — Custom fields:** Add 5 eSignature tracking fields to `Appraiser_Case__c` object metadata and update `package.xml` if needed. Deploy to verify. (FR-001) - [x] **Task 1 — Custom fields:** Add 5 eSignature tracking fields to `Appraiser_Case__c` object metadata and update `package.xml` if needed. Deploy to verify. (FR-001)
- [ ] **Task 2 — EnvelopeCreateResult class + createEnvelope() method:** Add `EnvelopeCreateResult` inner class and `createEnvelope(Id caseId, String accountCode, String templateId)` to `DocusignESignatureService`. (FR-002) - [x] **Task 2 — EnvelopeCreateResult class + createEnvelope() method:** Add `EnvelopeCreateResult` inner class and `createEnvelope(Id caseId, String accountCode, String templateId)` to `DocusignESignatureService`. (FR-002)
- [ ] **Task 3 — Tests for createEnvelope():** Add test coverage in `DocusignESignatureServiceTest` — success path (mock 201), failure path (mock 400), blank email guard. (NFR-001, TC-001, TC-002) - [ ] **Task 3 — Tests for createEnvelope():** Add test coverage in `DocusignESignatureServiceTest` — success path (mock 201), failure path (mock 400), blank email guard. (NFR-001, TC-001, TC-002)

View File

@ -43,6 +43,14 @@ public with sharing class DocusignESignatureService {
@AuraEnabled public String rawJson; @AuraEnabled public String rawJson;
} }
public class EnvelopeCreateResult {
@AuraEnabled public Boolean success;
@AuraEnabled public String envelopeId;
@AuraEnabled public String status;
@AuraEnabled public String envelopeUri;
@AuraEnabled public String errorMessage;
}
@AuraEnabled(cacheable=true) @AuraEnabled(cacheable=true)
public static ESignatureAccountConfig getAccountConfig(String accountCode) { public static ESignatureAccountConfig getAccountConfig(String accountCode) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
@ -144,6 +152,78 @@ public with sharing class DocusignESignatureService {
return parseEnvelopeList(response.responseBody); return parseEnvelopeList(response.responseBody);
} }
@AuraEnabled(cacheable=false)
public static EnvelopeCreateResult createEnvelope(Id caseId, String accountCode, String templateId) {
EnvelopeCreateResult result = new EnvelopeCreateResult();
result.success = false;
try {
if (String.isBlank(templateId)) {
result.errorMessage = 'Template ID is required.';
return result;
}
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
String targetAccountId = requireESignatureAccountId(row, accountCode, null);
Appraiser_Case__c caseRecord = [
SELECT Appraiser_Email__c,
Appraiser_Name__c,
Appraiser_Last_Name__c,
Appraiser_Salutation__c
FROM Appraiser_Case__c
WHERE Id = :caseId
LIMIT 1
];
if (String.isBlank(caseRecord.Appraiser_Email__c)) {
result.errorMessage = 'Appraiser email is blank on this case. Cannot create envelope.';
return result;
}
String appraiserName = String.isNotBlank(caseRecord.Appraiser_Name__c)
? caseRecord.Appraiser_Name__c
: ((String.isNotBlank(caseRecord.Appraiser_Salutation__c)
? caseRecord.Appraiser_Salutation__c + ' '
: '') + String.valueOf(caseRecord.Appraiser_Last_Name__c)).trim();
Map<String, Object> roleMap = new Map<String, Object>{
'email' => caseRecord.Appraiser_Email__c,
'name' => appraiserName,
'roleName' => 'Signer'
};
Map<String, Object> bodyMap = new Map<String, Object>{
'templateId' => templateId,
'templateRoles' => new List<Object>{ roleMap },
'status' => 'sent'
};
HttpRequest req = new HttpRequest();
req.setEndpoint(buildEndpoint(
'/v2.1/accounts/' + targetAccountId + '/envelopes',
row.ESignature_Rest_Named_Credential__c
));
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(30000);
req.setBody(JSON.serialize(bodyMap));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
result.success = true;
result.envelopeId = (String) responseMap.get('envelopeId');
result.status = (String) responseMap.get('status');
result.envelopeUri = (String) responseMap.get('uri');
} else {
result.errorMessage = 'eSignature API Error (HTTP ' + res.getStatusCode() + '): ' + res.getBody();
}
} catch (Exception e) {
result.success = false;
result.errorMessage = e.getMessage();
}
return result;
}
@TestVisible @TestVisible
private static List<ESignatureAccountSummary> parseAccountList(String body) { private static List<ESignatureAccountSummary> parseAccountList(String body) {
Object root = JSON.deserializeUntyped(body); Object root = JSON.deserializeUntyped(body);