Functions
GetEnvironmentVariableValueString
public string GetEnvironmentVariableValueString(string schemanameEnvVarDefinition)
{
tracingService.Trace("Calling GetEnvironmentVariableValueString");
// Retrieve Environment Variable Definition (i.e. default)
QueryExpression queryEnvVarDefinition = new QueryExpression("environmentvariabledefinition")
{
ColumnSet = new ColumnSet("defaultvalue", "environmentvariabledefinitionid")
};
queryEnvVarDefinition.Criteria.AddCondition("schemaname", ConditionOperator.Equal, schemanameEnvVarDefinition);
EntityCollection collectionEnvVarDefinition = crmService.RetrieveMultiple(queryEnvVarDefinition);
if (collectionEnvVarDefinition.Entities.Count != 1)
{
throw new Exception("Environment variable not found.");
}
Entity entityEnvVarDefinition = collectionEnvVarDefinition.Entities[0];
string defaultvalueEnvVarDefinition = entityEnvVarDefinition.GetAttributeValue<string>("defaultvalue");
// Retrieve Environment Variable Value (i.e. current)
QueryExpression queryEnvVarCurrentValue = new QueryExpression("environmentvariablevalue")
{
ColumnSet = new ColumnSet("value")
};
queryEnvVarCurrentValue.Criteria.AddCondition("environmentvariabledefinitionid", ConditionOperator.Equal, entityEnvVarDefinition.Id);
EntityCollection collectionEnvVarCurrentValue = crmService.RetrieveMultiple(queryEnvVarCurrentValue);
if (collectionEnvVarCurrentValue.Entities.Count != 1)
{
// Return current value if it exists, otherwise return default value
return defaultvalueEnvVarDefinition;
}
Entity entityEnvVarCurrentValue = collectionEnvVarCurrentValue.Entities[0];
string valueEnvVarCurrentValue = entityEnvVarCurrentValue.GetAttributeValue<string>("value");
return valueEnvVarCurrentValue;
}
TaxReturnChangeStatus
public void TaxReturnChangeStatus(EntityReference taxreturn, OptionSetValue state)
{
//Cascade tax return status to it's child records
string[] TaxReturnSchedules =
{
"tt_bankinterest",
"tt_capitalgain",
"tt_employment",
"tt_giftaid",
"tt_pensionincome",
"tt_personalpension",
"tt_propertyincome",
"tt_businesstax",
"tt_ukdividend"
};
foreach(string TaxReturnSchedule in TaxReturnSchedules)
{
tracingService.Trace($"SetState for {TaxReturnSchedule} for TaxReturn {taxreturn.Id}");
QueryExpression qe = new QueryExpression()
{
EntityName = TaxReturnSchedule
};
qe.Criteria.AddCondition("tt_taxreturnid", ConditionOperator.Equal, taxreturn.Id);
EntityCollection ScheduleRecords = crmService.RetrieveMultiple(qe);
foreach (Entity ScheduleRecord in ScheduleRecords.Entities)
{
SetStateRequest req = new SetStateRequest()
{
EntityMoniker = ScheduleRecord.ToEntityReference(),
State = state,
Status = new OptionSetValue(state.Value == 0 ? 1 : 2)
};
crmService.Execute(req);
}
tracingService.Trace($"{ScheduleRecords.Entities.Count} record(s) updated.");
}
}
RemoveWebroleWhenConnectionDeleted
public void RemoveWebroleWhenConnectionDeleted(Entity image) //PGF-918
{
tracingService.Trace("Entering RemoveWebroleWhenConnectionDeleted()");
Connection c = image.ToEntity<Connection>();
tracingService.Trace($"c = {JsonConvert.SerializeObject(c, Formatting.Indented)}");
if ((bool)c.IsMaster)
{
tracingService.Trace("This is the Master connection");
string thisRole = c.Record2RoleId.Name;
tracingService.Trace($"ConnectionRole: {thisRole}");
if(new List<string> {
"Accounts - Data Approver", "Accounts - Data Provider", "Accounts - Documents Only", "Accounts - Read Only",
"Audit - Data Approver", "Audit - Data Provider", "Audit - Documents Only", "Audit - Primary Contact", "Audit - Read Only", "Business Advisory - Basic User",
"Business Advisory - Data Approver", "Business Advisory - Data Provider", "Business Advisory - Documents Only", "Business Advisory - Primary Contact", "Business Advisory - Read Only",
"Business Tax - Data Approver", "Business Tax - Data Provider", "Business Tax - Documents Only", "Business Tax - Read Only",
"Calendar - Data Approver", "Calendar - Data Provider", "Calendar - Read Only",
"Champion", "Child", "Colleague", "Component", "Connected IoT Device",
"Corporate Finance - Basic User", "Corporate Finance - Corporate Contact", "Corporate Finance - Data Approver", "Corporate Finance - Data Provider", "Corporate Finance - Documents Only", "Corporate Finance - Primary Contact", "Corporate Finance - Read Only",
"Dashboard - Corporates", "Dashboard - Corporation", "Dashboard - Employer Solutions", "Dashboard - R&D",
"Data Approver", "Data Provider",
"Digital - Data Approver", "Digital - Data Provider", "Digital - Documents Only", "Digital - Primary Contact", "Digital - Read Only",
"Employment Tax - Data Approver", "Employment Tax - Data Provider", "Employment Tax - Documents Only", "Employment Tax - Read Only",
"Funds Tax - Data Approver", "Funds Tax - Data Provider", "Funds Tax - Documents Only", "Funds Tax - Read Only",
"Ireland Business Tax - Data Approver", "Ireland Business Tax - Data Provider", "Ireland Business Tax - Documents Only", "Ireland Business Tax - Primary Contact", "Ireland Business Tax - Read Only",
"Ireland Tax - Basic User", "Ireland Tax - Data Approver", "Ireland Tax - Data Provider", "Ireland Tax - Documents Only", "Ireland Tax - Read Only",
"Organisation - Data Approver", "Organisation - Data Provider", "Organisation - Documents Only", "Organisation - Primary Contact", "Organisation - Read Only",
"Partnership - Basic User", "Partnership - Data Approver", "Partnership - Data Provider", "Partnership - Documents Only", "Partnership - Primary Contact", "Partnership - Read Only",
"Payroll - Basic User", "Payroll - Data Approver", "Payroll - Data Provider", "Payroll - Documents Only", "Payroll - Primary Contact", "Payroll - Read Only",
"RandD - Data Approver", "RandD - Data Provider", "RandD - Documents Only", "RandD - Primary Contact", "RandD - Read Only",
"Risk And Assurance - Basic User", "Risk And Assurance - Corporate Contact", "Risk And Assurance - Data Approver", "Risk And Assurance - Data Provider", "Risk And Assurance - Documents Only", "Risk And Assurance - Primary Contact", "Risk And Assurance - Read Only",
"Trust - Data Approver", "Trust - Data Provider", "Trust - Documents Only", "Trust - Lead Trustee", "Trust - Read Only",
"US Tax - Data Approver", "US Tax - Data Provider", "US Tax - US Tax Contact",
"Virtual Finance Office - Data Approver", "Virtual Finance Office - Data Provider", "Virtual Finance Office - Documents Only", "Virtual Finance Office - Read Only"
}.Contains(thisRole))
{
//Does the contact have other connections with this role?
QueryExpression qryConnection = new QueryExpression("connection");
qryConnection.Criteria.AddCondition("record1id", ConditionOperator.Equal, c.Record1Id.Id);
qryConnection.Criteria.AddCondition("record2roleid", ConditionOperator.Equal, c.Record2RoleId.Id);
if(crmService.RetrieveMultiple(qryConnection).Entities.Count == 0)
{
//No other connections - we can disassociate the webrole
tracingService.Trace($"No other connections found from {c.Record1Id.Name} as role {c.Record2RoleId.Name}. Ok to Disassociate.");
//Find the webrole with same name as connection role
QueryExpression qryWebrole = new QueryExpression("adx_webrole");
qryWebrole.Criteria.AddCondition("adx_name", ConditionOperator.Equal, thisRole);
EntityCollection ec = crmService.RetrieveMultiple(qryWebrole);
if (ec.Entities.Count != 1)
{
tracingService.Trace($"Found {ec.Entities.Count} matching webroles - can't Disassociate");
return;
}
//Disassociate webrole from contact
tracingService.Trace($"Found webrole to Disassociate: contactid = {c.Record1Id.Id}, adx_webroleid = {ec.Entities[0].Id}");
EntityReferenceCollection relatedEntities = new EntityReferenceCollection();
relatedEntities.Add(new EntityReference("adx_webrole", ec.Entities[0].Id));
crmService.Disassociate("contact", c.Record1Id.Id,
new Relationship("adx_webrole_contact"),
relatedEntities);
tracingService.Trace("Disassociate complete");
}
}
}
}
UpdateCompaniesHouseSkipStage
public void UpdateCompaniesHouseSkipStage(Entity postImage)
{
// TODO: refactor to remove image and pass in params instead
// Ensure post-image entity is valid
if (postImage == null)
{
throw new ArgumentNullException("postImage cannot be null");
}
tracingService.Trace("Processing post-image for Specific Risk Assessment");
// Retrieve Contact record associated with Specific Risk Assessment
if (!postImage.Contains("tt_contact"))
{
tracingService.Trace("Post-image does not contain 'tt_contact'. Exiting method.");
return;
}
EntityReference contactRef = postImage.GetAttributeValue<EntityReference>("tt_contact");
if (contactRef == null)
{
tracingService.Trace("tt_contact lookup is null. Exiting method.");
return;
}
tracingService.Trace($"Found Contact reference: {contactRef.Id}");
// Retrieve most recently created Companies House Data record associated with Contact
QueryExpression companiesHouseQuery = new QueryExpression("tt_companieshousedata")
{
ColumnSet = new ColumnSet("tt_companieshousedataid", "tt_kycforentitiesskipukbusinesssearchstage"),
Orders = { new OrderExpression("createdon", OrderType.Descending) }
};
companiesHouseQuery.Criteria.AddCondition("tt_contact", ConditionOperator.Equal, contactRef.Id);
EntityCollection companiesHouseRecords = crmService.RetrieveMultiple(companiesHouseQuery);
if (!companiesHouseRecords.Entities.Any())
{
tracingService.Trace("No Companies House Data records found for this Contact.");
return;
}
Entity latestCompaniesHouseRecord = companiesHouseRecords.Entities.First();
tracingService.Trace($"Updating Companies House Data record {latestCompaniesHouseRecord.Id}");
// Retrieve "skip stage" flag from post-image entity
if (!postImage.Contains("tt_kycforentitiesskipukbusinesssearchstage"))
{
tracingService.Trace("Post-image does not contain 'tt_kycforentitiesskipukbusinesssearchstage'. Exiting method.");
return;
}
bool skipStage = postImage.GetAttributeValue<bool>("tt_kycforentitiesskipukbusinesssearchstage");
// Update "tt_kycforentitiesskipukbusinesssearchstage" in Companies House Data record
Entity updateEntity = new Entity("tt_companieshousedata", latestCompaniesHouseRecord.Id)
{
["tt_kycforentitiesskipukbusinesssearchstage"] = skipStage
};
crmService.Update(updateEntity);
tracingService.Trace("Companies House Data record updated successfully.");
}
RetrieveSpecificRiskAssessmentOptionSetValueFromLabel
public int RetrieveSpecificRiskAssessmentOptionSetValueFromLabel(string targetStageSetting)
{
//TODO: remove hardcoding. use a RetrieveAttribute request and get the mapping for an input option set, then rename method accordingly
tracingService.Trace("Calling RetrieveSpecificRiskAssessmentOptionSetValueFromLabel");
// Return if no change required
Dictionary<string, int> stageMapping = new Dictionary<string, int>
{
{ "Risk routing", 206340000 },
{ "Include", 206340001 },
{ "Exclude", 206340002 }
};
if (!stageMapping.TryGetValue(targetStageSetting, out int expectedValue))
{
throw new Exception("No match in option set mapping. Exiting method.");
}
tracingService.Trace("Completed RetrieveSpecificRiskAssessmentOptionSetValueFromLabel");
return expectedValue;
}
UpdateSpecificRiskAssessmentIncludeMLROStage
public bool UpdateSpecificRiskAssessmentIncludeMLROStage(EntityReference specificRiskAssessment, string messageName, int mLROStageInclusion, int riskRating)
{
tracingService.Trace("Calling UpdateSpecificRiskAssessmentIncludeMLROStage");
// Determine if MLRO stage should be shown in KYC business process flow
bool newIncludeMLROStage;
if (messageName == "Create")
{
newIncludeMLROStage = (mLROStageInclusion == 206340001 /*Include*/);
}
else if (messageName == "Update")
{
newIncludeMLROStage = (mLROStageInclusion == 206340000 /*Risk routing*/ && riskRating == 206340001 /*Enhanced*/) || (mLROStageInclusion == 206340001 /*Include*/);
// TODO: refactor to a PreOperation step so we don't need to do a separate Update
Entity updateEntity = new Entity("tt_kyccheck", specificRiskAssessment.Id)
{
["tt_kycincludemlrostage"] = newIncludeMLROStage
};
crmService.Update(updateEntity);
}
else
{
throw new Exception("Unhandled message type.");
}
tracingService.Trace("Completed UpdateSpecificRiskAssessmentIncludeMLROStage");
return newIncludeMLROStage;
}
UpdateSpecificRiskAssessmentTriggerRiskRatingHandler
public void UpdateSpecificRiskAssessmentTriggerRiskRatingHandler(Entity postMessageImage)
{
tracingService.Trace("Calling UpdateSpecificRiskAssessmentTriggerRiskRatingHandler");
// Calling tables have an N:1 relationship with Specific Risk Assessment
EntityReference sraEntityReference = postMessageImage.GetAttributeValue<EntityReference>("tt_kyccheck");
if (sraEntityReference == null)
{
tracingService.Trace("tt_kyccheck lookup is null. Exiting method.");
return;
}
// Flag is detected during form OnLoad and triggers handler for tt_riskrating determination, which also unsets this flag
Entity updateEntity = new Entity("tt_kyccheck", sraEntityReference.Id)
{
["tt_triggerriskratinghandler"] = true
};
crmService.Update(updateEntity);
tracingService.Trace("Specific Risk Assessment updated.");
}
GetAssignmentConfiguration
Retrieves the assignment-to-web-role configuration stored in the tt_configuration entity. The configuration is expected to be a JSON string named "Assignments.json".
public Dictionary<string, Dictionary<string, List<string>>> GetAssignmentConfiguration(IOrganizationService service, ITracingService tracingService)
{
// Build query to retrieve the configuration record
QueryExpression configQuery = new QueryExpression("tt_configuration")
{
ColumnSet = new ColumnSet("tt_content"),
Criteria = new FilterExpression
{
Conditions = { new ConditionExpression("tt_name", ConditionOperator.Equal, "Assignments.json") }
}
};
// Execute query
EntityCollection configResults = service.RetrieveMultiple(configQuery);
if (configResults.Entities.Count == 0)
{
tracingService.Trace("Assignments.json configuration not found.");
return null;
}
try
{
// Parse JSON content into a nested dictionary structure
string jsonContent = configResults.Entities[0].GetAttributeValue<string>("tt_content");
return JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, List<string>>>>(jsonContent);
}
catch (Exception ex)
{
tracingService.Trace($"Failed to parse configuration JSON: {ex}");
throw new InvalidPluginExecutionException("Configuration parsing failed.", ex);
}
}
GetWebRoleByName
Retrieves a web role entity by its name from the adx_webrole entity.
public Entity GetWebRoleByName(IOrganizationService service, string webRoleName)
{
QueryExpression webRoleQuery = new QueryExpression("adx_webrole")
{
ColumnSet = new ColumnSet("adx_name"),
Criteria = new FilterExpression
{
Conditions = { new ConditionExpression("adx_name", ConditionOperator.Equal, webRoleName) }
}
};
EntityCollection results = service.RetrieveMultiple(webRoleQuery);
return results.Entities.FirstOrDefault(); // Return the first match or null
}
IsContactAssociatedWithWebRole
Checks whether a contact is already associated with a specific web role.
public bool IsContactAssociatedWithWebRole(IOrganizationService service, Guid contactId, Guid webRoleId)
{
QueryExpression associationCheck = new QueryExpression("adx_webrole_contact")
{
ColumnSet = new ColumnSet(false), // No need to retrieve columns
Criteria = new FilterExpression
{
Conditions =
{
new ConditionExpression("contactid", ConditionOperator.Equal, contactId),
new ConditionExpression("adx_webroleid", ConditionOperator.Equal, webRoleId)
}
}
};
return service.RetrieveMultiple(associationCheck).Entities.Count > 0;
}
ValidateContactAssignmentRelationship
Validates that the plugin context contains a valid tt_Contact_tt_Assignment relationship, and extracts the contact and assignment references.
public bool ValidateContactAssignmentRelationship(IPluginExecutionContext context, ITracingService tracingService, out EntityReference contact, out EntityReference assignment)
{
contact = null;
assignment = null;
// Check if the relationship is correct
if (!(context.InputParameters["Relationship"] is Relationship rel) ||
!string.Equals(rel.SchemaName, "tt_Contact_tt_Assignment", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Validate presence of required parameters
if (!(context.InputParameters["Target"] is EntityReference target) ||
!(context.InputParameters["RelatedEntities"] is EntityReferenceCollection related) ||
related.Count == 0)
{
tracingService.Trace("Missing Target or RelatedEntities parameters");
return false;
}
// Ensure correct entity types
if (target.LogicalName != "contact" || related[0].LogicalName != "tt_assignment")
{
tracingService.Trace("Entity type mismatch");
return false;
}
// Assign output values
contact = target;
assignment = related[0];
return true;
}
GetWebRolesForAssignment
Retrieves the list of web roles configured for a given assignment name from the configuration dictionary.
public List<string> GetWebRolesForAssignment(Dictionary<string, Dictionary<string, List<string>>> config, string assignmentName, ITracingService tracingService)
{
if (config.TryGetValue(assignmentName, out var assignmentConfig) &&
assignmentConfig.TryGetValue("Web Roles", out var webRoles))
{
tracingService.Trace($"Found {webRoles.Count} web roles for assignment '{assignmentName}'");
return webRoles;
}
tracingService.Trace($"No web roles found for assignment '{assignmentName}'");
return new List<string>();
}
GetOptionSetLabel
Retrieves the label for an option set value. Handles both standard picklists and status attributes.
public string GetOptionSetLabel(string entityName, string attributeName, int optionSetValue)
{
if (optionSetValue == -1)
{
return null;
}
try
{
var request = new RetrieveAttributeRequest
{
EntityLogicalName = entityName,
LogicalName = attributeName,
RetrieveAsIfPublished = true
};
var response = (RetrieveAttributeResponse)crmService.Execute(request);
if (response?.AttributeMetadata == null)
{
return $"Unknown ({optionSetValue})";
}
OptionMetadata matchedOption = null;
// Handle status attributes
if (response.AttributeMetadata is StatusAttributeMetadata statusMetadata)
{
foreach (var option in statusMetadata.OptionSet.Options)
{
if (option.Value == optionSetValue)
{
matchedOption = option;
break;
}
}
}
// Handle standard picklists
else if (response.AttributeMetadata is PicklistAttributeMetadata picklistMetadata)
{
foreach (var option in picklistMetadata.OptionSet.Options)
{
if (option.Value == optionSetValue)
{
matchedOption = option;
break;
}
}
}
if (matchedOption != null)
{
return matchedOption.Label?.UserLocalizedLabel?.Label ?? $"Unknown ({optionSetValue})";
}
return $"Unknown ({optionSetValue})";
}
catch (Exception ex)
{
tracingService.Trace($"GetOptionSetLabel failed for {entityName}.{attributeName}: {ex.Message}");
return $"Error ({optionSetValue})";
}
}
GetServiceLineLabel
Retrieves the service line label from a document approval record.
public string GetServiceLineLabel(Entity documentApproval)
{
int serviceLineValue = documentApproval.GetAttributeValue<OptionSetValue>("tt_serviceline")?.Value ?? -1;
string serviceLineLabel;
try
{
serviceLineLabel = GetOptionSetLabel("tt_documentapproval", "tt_serviceline", serviceLineValue);
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException($"Failed to retrieve service line label: {ex.Message}", ex);
}
if (string.IsNullOrEmpty(serviceLineLabel))
{
throw new InvalidPluginExecutionException($"Service Line label could not be retrieved for value {serviceLineValue}.");
}
return serviceLineLabel;
}
GetRoleFilterForServiceLine
Determines the role name filter and operator type based on the service line.
public (string roleNameFilter, string operatorType) GetRoleFilterForServiceLine(string serviceLineLabel)
{
switch (serviceLineLabel)
{
case "General":
return ("%Data Approver%", "like");
case "Funds Tax":
case "CTG Tax":
return ("CTG Tax - Data Approver", "eq");
case "Research And Development":
return ("RandD - Data Approver", "eq");
case "UK Tax":
return ("Data Approver", "eq");
default:
return ($"{serviceLineLabel} - Data Approver", "eq");
}
}
RetrieveConnectedContacts
Retrieves connected contacts with appropriate Data Approver roles using FetchXML.
public EntityCollection RetrieveConnectedContacts(Guid contactId, Guid documentApprovalId, string roleNameFilter, string operatorType)
{
string fetchXml = $@"
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='true'>
<entity name='contact'>
<attribute name='fullname' />
<attribute name='contactid' />
<link-entity name='connection' from='record2id' to='contactid' link-type='inner'>
<filter>
<condition attribute='record1roleidname' operator='{operatorType}' value='{roleNameFilter}' />
</filter>
<link-entity name='contact' from='contactid' to='record1id' link-type='inner'>
<link-entity name='tt_documentapproval' from='tt_contactid' to='contactid' link-type='inner'>
<filter>
<condition attribute='tt_contactid' operator='eq' value='{contactId}' />
<condition attribute='tt_documentapprovalid' operator='eq' value='{documentApprovalId}' />
</filter>
</link-entity>
</link-entity>
</link-entity>
</entity>
</fetch>";
try
{
EntityCollection connectedContacts = crmService.RetrieveMultiple(new FetchExpression(fetchXml));
tracingService.Trace($"Connected contacts found: {connectedContacts.Entities.Count}");
return connectedContacts;
}
catch (Exception ex)
{
tracingService.Trace($"FetchXML query failed: {ex.Message}");
throw new InvalidPluginExecutionException($"Failed to retrieve connected contacts: {ex.Message}", ex);
}
}
DeleteExistingSigners
Creates signer records for each connected contact.
public void CreateSignerRecords(Guid documentApprovalId, EntityCollection connectedContacts)
{
int successCount = 0;
int failureCount = 0;
foreach (var contact in connectedContacts.Entities)
{
string contactName = contact.GetAttributeValue<string>("fullname") ?? "Unknown";
Guid connectedContactId = contact.Id;
try
{
Entity signer = new Entity("tt_documentapprovaldocumentsigners");
signer["tt_documentapproval"] = new EntityReference("tt_documentapproval", documentApprovalId);
signer["tt_contact"] = new EntityReference("contact", connectedContactId);
crmService.Create(signer);
successCount++;
}
catch (Exception ex)
{
failureCount++;
tracingService.Trace($"Failed to create signer for Contact: {contactName} ({connectedContactId}): {ex.Message}");
// Continue processing remaining contacts
}
}
tracingService.Trace($"Signers created: {successCount}/{connectedContacts.Entities.Count} successful, {failureCount} failed");
if (failureCount > 0)
{
tracingService.Trace("WARNING: Some signers failed to create. See errors above.");
}
}
ValidateAndGetTarget
Validates the plugin execution context and returns the target entity. Checks entity type, message type, and depth to prevent infinite loops.
public Entity ValidateAndGetTarget(IPluginExecutionContext context, string expectedEntityName, string expectedMessage, int maxDepth = 10)
{
tracingService.Trace($"[ValidateAndGetTarget] Validating context for {expectedEntityName}/{expectedMessage}");
// Check depth to prevent infinite loops
if (context.Depth > maxDepth)
{
throw new InvalidPluginExecutionException($"Plugin depth ({context.Depth}) exceeds maximum allowed depth ({maxDepth}). Possible infinite loop detected.");
}
// Validate Target parameter exists
if (!context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
{
throw new InvalidPluginExecutionException("Target entity is missing or invalid.");
}
Entity target = (Entity)context.InputParameters["Target"];
// Validate entity and message types
if (target.LogicalName != expectedEntityName || context.MessageName != expectedMessage)
{
throw new InvalidPluginExecutionException(
$"Plugin triggered on incorrect entity or message. Expected: {expectedEntityName}/{expectedMessage}, Got: {target.LogicalName}/{context.MessageName}");
}
tracingService.Trace($"[ValidateAndGetTarget] Validation successful. Target ID: {target.Id}");
return target;
}
RetrieveDocumentApproval
Retrieves a document approval record with specified columns. Always retrieves core columns (tt_contactid, tt_serviceline) plus any additional columns specified.
public Entity RetrieveDocumentApproval(Guid documentApprovalId, params string[] additionalColumns)
{
tracingService.Trace($"[RetrieveDocumentApproval] Retrieving document approval: {documentApprovalId}");
try
{
// Build column set with core columns
List<string> columns = new List<string> { "tt_contactid", "tt_serviceline" };
// Add any additional columns requested
if (additionalColumns != null && additionalColumns.Length > 0)
{
columns.AddRange(additionalColumns);
tracingService.Trace($"[RetrieveDocumentApproval] Additional columns: {string.Join(", ", additionalColumns)}");
}
Entity documentApproval = crmService.Retrieve("tt_documentapproval", documentApprovalId, new ColumnSet(columns.ToArray()));
tracingService.Trace($"[RetrieveDocumentApproval] Successfully retrieved document approval");
return documentApproval;
}
catch (Exception ex)
{
tracingService.Trace($"[RetrieveDocumentApproval] Error: {ex.Message}");
throw new InvalidPluginExecutionException($"Failed to retrieve document approval record {documentApprovalId}: {ex.Message}", ex);
}
}
GetContactIdFromDocumentApproval
Extracts and validates the contact ID from a document approval record.
public Guid GetContactIdFromDocumentApproval(Entity documentApproval)
{
tracingService.Trace($"[GetContactIdFromDocumentApproval] Extracting contact ID");
if (documentApproval == null)
{
throw new ArgumentNullException(nameof(documentApproval), "Document Approval entity cannot be null.");
}
if (!documentApproval.Contains("tt_contactid") || documentApproval["tt_contactid"] == null)
{
throw new InvalidPluginExecutionException(
$"Document Approval record {documentApproval.Id} does not contain a valid tt_contactid reference.");
}
EntityReference contactRef = (EntityReference)documentApproval["tt_contactid"];
tracingService.Trace($"[GetContactIdFromDocumentApproval] Contact ID: {contactRef.Id}");
return contactRef.Id;
}
ValidateServiceLine
Validates that the document approval has a service line value.
public bool ValidateServiceLine(Entity documentApproval)
{
tracingService.Trace($"[ValidateServiceLine] Validating service line");
if (documentApproval == null)
{
throw new ArgumentNullException(nameof(documentApproval), "Document Approval entity cannot be null.");
}
if (!documentApproval.Contains("tt_serviceline") || documentApproval["tt_serviceline"] == null)
{
tracingService.Trace($"[ValidateServiceLine] Service line is missing or null");
return false;
}
OptionSetValue serviceLineValue = documentApproval.GetAttributeValue<OptionSetValue>("tt_serviceline");
if (serviceLineValue == null || serviceLineValue.Value <= 0)
{
tracingService.Trace($"[ValidateServiceLine] Service line has invalid value");
return false;
}
tracingService.Trace($"[ValidateServiceLine] Service line is valid: {serviceLineValue.Value}");
return true;
}
IsDocumentApprovalInDraft
Checks if a document approval is in Draft status.
public bool IsDocumentApprovalInDraft(Entity documentApproval, int draftStatusCode = 206340002)
{
tracingService.Trace($"[IsDocumentApprovalInDraft] Checking draft status");
if (documentApproval == null)
{
throw new ArgumentNullException(nameof(documentApproval), "Document Approval entity cannot be null.");
}
if (!documentApproval.Contains("statuscode"))
{
throw new InvalidPluginExecutionException(
$"Document Approval record {documentApproval.Id} does not contain statuscode attribute.");
}
int statusCode = documentApproval.GetAttributeValue<OptionSetValue>("statuscode")?.Value ?? -1;
string statusLabel = GetOptionSetLabel("tt_documentapproval", "statuscode", statusCode);
bool isDraft = statusLabel.Equals("Draft", StringComparison.OrdinalIgnoreCase);
tracingService.Trace($"[IsDocumentApprovalInDraft] Status: {statusLabel} ({statusCode}), Is Draft: {isDraft}");
return isDraft;
}
UpdateSignersForDocumentApproval
Orchestrates the complete signer update process: retrieves contacts, deletes old signers, creates new signers. Provides transactional-like behavior with detailed error reporting.
public int UpdateSignersForDocumentApproval(Guid documentApprovalId, Guid contactId, string serviceLineLabel, bool deleteExisting = false)
{
tracingService.Trace($"[UpdateSignersForDocumentApproval] Starting signer update process");
tracingService.Trace($"[UpdateSignersForDocumentApproval] Document Approval: {documentApprovalId}, Contact: {contactId}, Service Line: {serviceLineLabel}");
try
{
// Get role filter based on service line
var (roleNameFilter, operatorType) = GetRoleFilterForServiceLine(serviceLineLabel);
tracingService.Trace($"[UpdateSignersForDocumentApproval] Role Filter: {roleNameFilter}, Operator: {operatorType}");
// Delete existing signers if requested
if (deleteExisting)
{
tracingService.Trace($"[UpdateSignersForDocumentApproval] Deleting existing signers");
DeleteExistingSigners(documentApprovalId);
}
// Retrieve connected contacts
EntityCollection connectedContacts = RetrieveConnectedContacts(contactId, documentApprovalId, roleNameFilter, operatorType);
if (connectedContacts.Entities.Count == 0)
{
tracingService.Trace($"[UpdateSignersForDocumentApproval] No connected contacts found matching criteria");
return 0;
}
// Create signer records
tracingService.Trace($"[UpdateSignersForDocumentApproval] Creating {connectedContacts.Entities.Count} signer record(s)");
CreateSignerRecords(documentApprovalId, connectedContacts);
tracingService.Trace($"[UpdateSignersForDocumentApproval] Signer update process completed successfully");
return connectedContacts.Entities.Count;
}
catch (Exception ex)
{
tracingService.Trace($"[UpdateSignersForDocumentApproval] Error during signer update: {ex.Message}");
throw new InvalidPluginExecutionException(
$"Failed to update signers for Document Approval {documentApprovalId}: {ex.Message}", ex);
}
}