Get Access to the full Apex Classes for Prehook and Posthook examples
Get access to an Apex prehook class that demonstrates how to:
- Dynamically calculate a technical attribute (“MaxPayload”) based on other user-selected product attributes (like ‘Payload_Capacity_Requirement’, ‘Operating_Mode’, ‘TerrainAdaptationLevel’, and ‘MobilityType’) using custom logic and lookup maps directly within Apex. This allows for calculations far more complex than standard declarative rules.
- Persist this calculated attribute value back into the pricing context so it can be used by subsequent pricing rules or other processes.
- Systematically copy multiple attribute values (including the newly calculated “MaxPayload” and others like “Operating_Mode”) from the product’s attribute set directly onto corresponding custom fields on the parent Quote Line (or Order Line).
- Propagate these attribute values and the calculated “MaxPayload” field consistently from a parent (bundle) product to all its child line items (options/components), ensuring data accuracy across the entire product structure.
- Implement a crucial best practice: How to check the
dmlStatusof each line item and skip processing for any lines marked as ‘DELETED’, preventing errors and ensuring the hook only operates on active lines. - Correctly structure the
dataPathfor updating bothSalesTransactionItemAttributenodes (the attributes themselves) andSalesTransactionItemnodes (the line item fields) within the Revenue Cloud’s context API.
Get access to an Apex post-hook class that illustrates how to:
Ensure this custom description generation works accurately for both standalone products and products within bundles, including all child line items (options/components), by correctly sourcing attribute values based on the product hierarchy.
Read the final values of multiple product attributes (such as ‘Payload_Capacity_Requirement’, ‘Operating_Mode’, ‘MaxPayload’, etc.) from each quote line after all standard pricing calculations and any pre-hooks have completed.
Concatenate these diverse attribute values into a single, formatted descriptive string for each line item, providing a rich, consolidated summary of the configured product.
Update a standard or custom description field on each Quote Line (or Order Line) with this dynamically generated, comprehensive description.
Apex Prehook Example
global class ApexPreHookPayloadCapacity implements RevSignaling.SignalingApexProcessor {
public virtual class BaseException extends Exception {}
public class OtherException extends BaseException {}
// Define maps for lookups for MaxPayload calculation
private static final Map<String, Decimal> BASE_CAPACITY_MAP = new Map<String, Decimal>{
'Light' => 10,
'Medium' => 20,
'Heavy' => 30
};
private static final Map<String, Decimal> TERRAIN_FACTOR_MAP = new Map<String, Decimal>{
'Low' => 1.0,
'Medium' => 0.9,
'High' => 0.8
};
private static final Map<String, Decimal> MOBILITY_FACTOR_MAP = new Map<String, Decimal>{
'Wheeled' => 1.0,
'Tracked' => 0.95,
'Legged' => 0.85
};
// Attributes to be read from SalesTransactionItemAttribute
private static final Set<String> INPUT_ATTRIBUTES_FOR_CALC_AND_MAPPING = new Set<String>{
'Payload_Capacity_Requirement',
'Operating_Mode',
'TerrainAdaptationLevel',
'MobilityType' // For MaxPayload calculation
};
private static final String DEBUG_VERSION = 'V5.6'; // Keep your original version or update as needed
public RevSignaling.TransactionResponse execute(RevSignaling.TransactionRequest request) {
System.debug('Executing PREHOOK - Update MaxPayload & Map to SalesItem Fields (Parent & Child) - ' + DEBUG_VERSION);
String contextId = request.ctxInstanceId;
Context.IndustriesContext industriesContext = new Context.IndustriesContext();
// STEP 1: Query SalesTransactionItemAttribute
Map<String, Object> inputQueryItemAttr = new Map<String, Object>{
'contextId' => contextId,
'tags' => new List<String>{'SalesTransactionItemAttribute'}
};
Map<String, Object> itemAttrQueryOutput = industriesContext.queryTags(inputQueryItemAttr);
Map<String, Object> itemAttrQueryResult = (Map<String, Object>) itemAttrQueryOutput.get('queryResult');
List<Object> itemAttrDataList = (List<Object>) itemAttrQueryResult.get('SalesTransactionItemAttribute');
System.debug(DEBUG_VERSION + ' - SalesTransactionItemAttribute Raw Query: ' + JSON.serializePretty(itemAttrQueryResult));
Map<String, Map<String, String>> qliToSourceAttributeValues = new Map<String, Map<String, String>>();
Map<String, List<Object>> qliToMaxPayloadAttrDataPath = new Map<String, List<Object>>();
if (itemAttrDataList != null) {
for (Object attrObj : itemAttrDataList) {
Map<String, Object> attrNode = (Map<String, Object>) attrObj;
Map<String, Object> tagMap = (Map<String, Object>) attrNode.get('tagValue');
List<Object> currentAttrDataPath = (List<Object>) attrNode.get('dataPath');
String attributeName = (tagMap.containsKey('Attribute')) ? (String)((Map<String, Object>)tagMap.get('Attribute')).get('tagValue') : null;
String attributeValueStr = (tagMap.containsKey('AttributeValue')) ? (String)((Map<String, Object>)tagMap.get('AttributeValue')).get('tagValue') : null;
String parentSalesTrxItemCtxId = (tagMap.containsKey('SalesTransactionItemAttrParent')) ? (String)((Map<String, Object>)tagMap.get('SalesTransactionItemAttrParent')).get('tagValue') : null;
if (parentSalesTrxItemCtxId == null) {
System.debug(DEBUG_VERSION + ' - ParentCtxId (QLI_ID) missing for attr: ' + attributeName);
continue;
}
if (!qliToSourceAttributeValues.containsKey(parentSalesTrxItemCtxId)) {
qliToSourceAttributeValues.put(parentSalesTrxItemCtxId, new Map<String, String>());
}
if (attributeName != null && INPUT_ATTRIBUTES_FOR_CALC_AND_MAPPING.contains(attributeName) && attributeValueStr != null) {
qliToSourceAttributeValues.get(parentSalesTrxItemCtxId).put(attributeName, attributeValueStr);
}
if (attributeName == 'MaxPayload' && currentAttrDataPath != null) {
qliToMaxPayloadAttrDataPath.put(parentSalesTrxItemCtxId, currentAttrDataPath);
}
}
}
System.debug(DEBUG_VERSION + ' - qliToSourceAttributeValues: ' + JSON.serializePretty(qliToSourceAttributeValues));
System.debug(DEBUG_VERSION + ' - qliToMaxPayloadAttrDataPath: ' + JSON.serializePretty(qliToMaxPayloadAttrDataPath));
// STEP 2: Query SalesTransactionItem nodes
Map<String, Object> inputQueryItem = new Map<String, Object>{'contextId' => contextId, 'tags' => new List<String>{'SalesTransactionItem'}};
Map<String, Object> salesItemQueryOutput = industriesContext.queryTags(inputQueryItem);
Map<String, Object> salesItemQueryResult = (Map<String, Object>) salesItemQueryOutput.get('queryResult');
List<Object> salesItemDataList = (List<Object>) salesItemQueryResult.get('SalesTransactionItem');
System.debug(DEBUG_VERSION + ' - SalesTransactionItem Raw Query: ' + JSON.serializePretty(salesItemQueryResult));
// STEP 3: Prepare updates
List<Map<String, Object>> allNodeUpdates = new List<Map<String, Object>>();
Map<String, String> sourceQliToCalculatedMaxPayload = new Map<String, String>();
Set<String> processedSourceQliForMaxPayloadAttrUpdate = new Set<String>();
if (salesItemDataList != null) {
// Pre-computation Loop: Calculate MaxPayload for source QLIs and queue their MaxPayload attribute updates
for (Object itemObj : salesItemDataList) {
Map<String, Object> salesItemNode = (Map<String, Object>) itemObj;
Map<String, Object> salesItemTags = (Map<String, Object>) salesItemNode.get('tagValue');
if (salesItemTags == null || !salesItemTags.containsKey('LineItemPath') || !salesItemTags.containsKey('LineItem')) {
continue;
}
String lineItemTagValue = (String)((Map<String, Object>)salesItemTags.get('LineItem')).get('tagValue');
String lineItemPathTagValue = (String)((Map<String, Object>)salesItemTags.get('LineItemPath')).get('tagValue');
String currentLoopItemQliIdOrRef = lineItemTagValue;
String qliForAttributeLookup;
if (lineItemPathTagValue != null && lineItemPathTagValue.contains('/')) {
qliForAttributeLookup = lineItemPathTagValue.split('/')[0];
} else {
qliForAttributeLookup = currentLoopItemQliIdOrRef;
}
if (qliToSourceAttributeValues.containsKey(qliForAttributeLookup) && !sourceQliToCalculatedMaxPayload.containsKey(qliForAttributeLookup)) {
Map<String, String> sourceAttrs = qliToSourceAttributeValues.get(qliForAttributeLookup);
String payloadCapReqVal = sourceAttrs.get('Payload_Capacity_Requirement');
String terrainAdaptVal = sourceAttrs.get('TerrainAdaptationLevel');
String mobilityTypeVal = sourceAttrs.get('MobilityType');
String calculatedMaxPayload = null;
Decimal baseCapacity = 0; Decimal terrainFactor = 1.0; Decimal mobilityFactor = 1.0;
if (payloadCapReqVal != null && BASE_CAPACITY_MAP.containsKey(payloadCapReqVal)) baseCapacity = BASE_CAPACITY_MAP.get(payloadCapReqVal);
if (terrainAdaptVal != null && TERRAIN_FACTOR_MAP.containsKey(terrainAdaptVal)) terrainFactor = TERRAIN_FACTOR_MAP.get(terrainAdaptVal);
if (mobilityTypeVal != null && MOBILITY_FACTOR_MAP.containsKey(mobilityTypeVal)) mobilityFactor = MOBILITY_FACTOR_MAP.get(mobilityTypeVal);
if (baseCapacity > 0) {
Decimal maxPayloadDecimal = baseCapacity * terrainFactor * mobilityFactor;
calculatedMaxPayload = String.valueOf(maxPayloadDecimal.setScale(2));
}
sourceQliToCalculatedMaxPayload.put(qliForAttributeLookup, calculatedMaxPayload);
System.debug(DEBUG_VERSION + ' - Calculated MaxPayload for source QLI ' + qliForAttributeLookup + ': ' + calculatedMaxPayload);
if (calculatedMaxPayload != null && !processedSourceQliForMaxPayloadAttrUpdate.contains(qliForAttributeLookup)) {
List<Object> sourceMaxPayloadAttrPathFull = qliToMaxPayloadAttrDataPath.get(qliForAttributeLookup);
if (sourceMaxPayloadAttrPathFull != null) {
List<Object> pathForAttrUpdate = new List<Object>(sourceMaxPayloadAttrPathFull);
if(!pathForAttrUpdate.isEmpty()) pathForAttrUpdate.remove(0);
Map<String, Object> maxPayloadAttrUpdateNode = new Map<String, Object>{
'nodePath' => new Map<String, Object>{'dataPath' => pathForAttrUpdate},
'attributes' => new List<Object>{ new Map<String, Object>{
'attributeName' => 'AttributeValue', 'attributeValue' => calculatedMaxPayload
}}
};
allNodeUpdates.add(maxPayloadAttrUpdateNode);
processedSourceQliForMaxPayloadAttrUpdate.add(qliForAttributeLookup);
System.debug(DEBUG_VERSION + ' - Queued Update for Source MaxPayload Attribute. QLI: ' + qliForAttributeLookup + ', Value: ' + calculatedMaxPayload);
} else {
System.debug(DEBUG_VERSION + ' - No existing "MaxPayload" attribute path found for source QLI: ' + qliForAttributeLookup);
}
}
}
}
// Main Update Loop: Apply field updates to all items
for (Object itemObj : salesItemDataList) {
Map<String, Object> salesItemNode = (Map<String, Object>) itemObj;
List<Object> salesItemDataPathFull = (List<Object>) salesItemNode.get('dataPath');
String currentItemNodePathId = (salesItemDataPathFull != null && !salesItemDataPathFull.isEmpty()) ? String.valueOf(salesItemDataPathFull.get(salesItemDataPathFull.size()-1)) : null;
Map<String, Object> currentSalesItemTags = (Map<String, Object>) salesItemNode.get('tagValue');
if (currentSalesItemTags == null || !currentSalesItemTags.containsKey('LineItemPath') || !currentSalesItemTags.containsKey('LineItem')) {
System.debug(DEBUG_VERSION + ' - LineItemPath or LineItem tag missing for item node with path ID: ' + currentItemNodePathId);
continue;
}
// **MODIFICATION START: Check DML Status**
String dmlStatusOfItem = null;
// Attempt to get dmlStatus from the LineItemPath tag, as observed in logs
Map<String, Object> lineItemPathTagDetails = (Map<String, Object>)currentSalesItemTags.get('LineItemPath');
if (lineItemPathTagDetails != null && lineItemPathTagDetails.containsKey('dmlStatus')) {
dmlStatusOfItem = (String)lineItemPathTagDetails.get('dmlStatus');
} else {
// Fallback or alternative: Check dmlStatus of LineItem tag if LineItemPath doesn't have it
Map<String, Object> lineItemTagDetails = (Map<String, Object>)currentSalesItemTags.get('LineItem');
if (lineItemTagDetails != null && lineItemTagDetails.containsKey('dmlStatus')) {
dmlStatusOfItem = (String)lineItemTagDetails.get('dmlStatus');
}
}
if ('DELETED'.equals(dmlStatusOfItem)) {
System.debug(DEBUG_VERSION + ' - Skipping update for DELETED SalesTransactionItem node with path ID: ' + currentItemNodePathId);
continue;
}
// **MODIFICATION END**
List<Object> salesItemPathForNodeUpdate = new List<Object>();
if(salesItemDataPathFull != null && salesItemDataPathFull.size() > 2) {
salesItemPathForNodeUpdate.add(salesItemDataPathFull.get(1));
salesItemPathForNodeUpdate.add(salesItemDataPathFull.get(2));
} else {
System.debug(DEBUG_VERSION + ' - Invalid dataPath for SalesItemFields update: ' + JSON.serialize(salesItemDataPathFull) + ' for item node ' + currentItemNodePathId);
continue;
}
String currentLineItemPathValue = (String)((Map<String, Object>)currentSalesItemTags.get('LineItemPath')).get('tagValue');
String actualQliOrRefOfCurrentItem = (String)((Map<String, Object>)currentSalesItemTags.get('LineItem')).get('tagValue');
String attrSourceQliId;
if (currentLineItemPathValue != null && currentLineItemPathValue.contains('/')) {
attrSourceQliId = currentLineItemPathValue.split('/')[0];
} else {
attrSourceQliId = actualQliOrRefOfCurrentItem;
}
Map<String, String> effectiveOrigAttrValues = qliToSourceAttributeValues.get(attrSourceQliId);
String effectiveCalculatedMaxPayload = sourceQliToCalculatedMaxPayload.get(attrSourceQliId);
if (effectiveOrigAttrValues == null) {
System.debug(DEBUG_VERSION + ' - No source attributes found for item node ' + currentItemNodePathId + ' (attrSourceQliId: ' + attrSourceQliId + ') for field updates.');
continue;
}
List<Map<String, Object>> salesItemFieldUpdates = new List<Map<String, Object>>();
if (effectiveOrigAttrValues.containsKey('Payload_Capacity_Requirement')) {
salesItemFieldUpdates.add(new Map<String, Object>{'attributeName' => 'PayloadCapacityRequirement__c', 'attributeValue' => effectiveOrigAttrValues.get('Payload_Capacity_Requirement')});
}
if (effectiveOrigAttrValues.containsKey('Operating_Mode')) {
salesItemFieldUpdates.add(new Map<String, Object>{'attributeName' => 'OperatingMode__c', 'attributeValue' => effectiveOrigAttrValues.get('Operating_Mode')});
}
if (effectiveOrigAttrValues.containsKey('TerrainAdaptationLevel')) {
salesItemFieldUpdates.add(new Map<String, Object>{'attributeName' => 'TerrainAdaptationLevel__c', 'attributeValue' => effectiveOrigAttrValues.get('TerrainAdaptationLevel')});
}
salesItemFieldUpdates.add(new Map<String, Object>{'attributeName' => 'MaxPayload__c', 'attributeValue' => effectiveCalculatedMaxPayload});
if (!salesItemFieldUpdates.isEmpty()) {
if(salesItemPathForNodeUpdate.size() == 2){
Map<String, Object> salesItemFieldsUpdateNode = new Map<String, Object>{
'nodePath' => new Map<String, Object>{'dataPath' => salesItemPathForNodeUpdate},
'attributes' => salesItemFieldUpdates
};
allNodeUpdates.add(salesItemFieldsUpdateNode);
System.debug(DEBUG_VERSION + ' - Queued SalesItem Fields Update for Item Node Path ID: ' + currentItemNodePathId + ' (using source QLI ' + attrSourceQliId + '), DataPath For Update: ' + JSON.serialize(salesItemPathForNodeUpdate));
} else {
System.debug(DEBUG_VERSION + ' - Invalid salesItemPathForNodeUpdate for Item Fields: ' + JSON.serialize(salesItemPathForNodeUpdate) + ' for item ' + currentItemNodePathId);
}
}
}
}
// STEP 4: Submit context update
RevSignaling.TransactionResponse response = new RevSignaling.TransactionResponse();
if (!allNodeUpdates.isEmpty()) {
Map<String, Object> updateInput = new Map<String, Object>{
'contextId' => contextId,
'nodePathAndAttributes' => allNodeUpdates
};
System.debug(DEBUG_VERSION + ' - Submitting Context Update: ' + JSON.serializePretty(updateInput));
try {
industriesContext.updateContextAttributes(updateInput);
response.status = RevSignaling.TransactionStatus.SUCCESS;
response.message = 'Pre-hook ' + DEBUG_VERSION + ': MaxPayload attribute and SalesItem fields updated for parents and children.';
} catch (Exception e) {
System.debug(DEBUG_VERSION + ' - ERROR during context update: ' + e.getMessage() + ' Stack: ' + e.getStackTraceString());
response.status = RevSignaling.TransactionStatus.FAILED;
response.message = 'Pre-hook ' + DEBUG_VERSION + ' Error: ' + e.getMessage();
}
} else {
response.status = RevSignaling.TransactionStatus.SUCCESS;
response.message = 'Pre-hook ' + DEBUG_VERSION + ': No updates were necessary.';
System.debug(DEBUG_VERSION + ' - No updates to submit.');
}
return response;
}
}
Apex Posthook Example
global class ApexPostHookUpdateDescription implements RevSignaling.SignalingApexProcessor {
public virtual class BaseException extends Exception {}
public class OtherException extends BaseException {}
// Define the set of attribute names to be concatenated for the description
private static final List<String> ATTRIBUTES_FOR_DESCRIPTION = new List<String>{
'Payload_Capacity_Requirement',
'Operating_Mode',
'TerrainAdaptationLevel',
'MobilityType',
'MaxPayload' // Assuming MaxPayload is also an attribute available at this stage
};
private static final String DEBUG_VERSION = 'V1.0_PostHookDesc';
private static final String DESCRIPTION_FIELD_API_NAME = 'SalesTrxnItemDescription'; // Or the correct API name for the description field
public RevSignaling.TransactionResponse execute(RevSignaling.TransactionRequest request) {
System.debug('Executing POSTHOOK - Update SalesTrxnItemDescription - ' + DEBUG_VERSION);
String contextId = request.ctxInstanceId;
Context.IndustriesContext industriesContext = new Context.IndustriesContext();
RevSignaling.TransactionResponse response = new RevSignaling.TransactionResponse();
List<Map<String, Object>> allNodeUpdates = new List<Map<String, Object>>();
try {
// STEP 1: Query SalesTransactionItemAttribute to get all relevant attribute values
Map<String, Object> inputQueryItemAttr = new Map<String, Object>{
'contextId' => contextId,
'tags' => new List<String>{'SalesTransactionItemAttribute'}
};
Map<String, Object> itemAttrQueryOutput = industriesContext.queryTags(inputQueryItemAttr);
Map<String, Object> itemAttrQueryResult = (Map<String, Object>) itemAttrQueryOutput.get('queryResult');
List<Object> itemAttrDataList = (List<Object>) itemAttrQueryResult.get('SalesTransactionItemAttribute');
System.debug(DEBUG_VERSION + ' - SalesTransactionItemAttribute Raw Query: ' + JSON.serializePretty(itemAttrQueryResult));
// Map to store attribute values per SalesTransactionItem (QLI)
// Key: parentSalesTrxItemCtxId (e.g., QLI Id)
// Value: Map of AttributeName -> AttributeValue
Map<String, Map<String, String>> qliToAttributeValues = new Map<String, Map<String, String>>();
if (itemAttrDataList != null) {
for (Object attrObj : itemAttrDataList) {
Map<String, Object> attrNode = (Map<String, Object>) attrObj;
Map<String, Object> tagMap = (Map<String, Object>) attrNode.get('tagValue');
String attributeName = (tagMap.containsKey('Attribute')) ? (String)((Map<String, Object>)tagMap.get('Attribute')).get('tagValue') : null;
String attributeValueStr = (tagMap.containsKey('AttributeValue')) ? (String)((Map<String, Object>)tagMap.get('AttributeValue')).get('tagValue') : null;
String parentSalesTrxItemCtxId = (tagMap.containsKey('SalesTransactionItemAttrParent')) ? (String)((Map<String, Object>)tagMap.get('SalesTransactionItemAttrParent')).get('tagValue') : null;
if (parentSalesTrxItemCtxId == null) {
System.debug(DEBUG_VERSION + ' - ParentCtxId (QLI_ID) missing for attribute: ' + attributeName);
continue;
}
if (!qliToAttributeValues.containsKey(parentSalesTrxItemCtxId)) {
qliToAttributeValues.put(parentSalesTrxItemCtxId, new Map<String, String>());
}
// Store the attribute if it's one we need for the description
if (attributeName != null && ATTRIBUTES_FOR_DESCRIPTION.contains(attributeName) && attributeValueStr != null) {
qliToAttributeValues.get(parentSalesTrxItemCtxId).put(attributeName, attributeValueStr);
}
}
}
System.debug(DEBUG_VERSION + ' - qliToAttributeValues: ' + JSON.serializePretty(qliToAttributeValues));
// STEP 2: Query SalesTransactionItem nodes to update their description
Map<String, Object> inputQueryItem = new Map<String, Object>{
'contextId' => contextId,
'tags' => new List<String>{'SalesTransactionItem'}
};
Map<String, Object> salesItemQueryOutput = industriesContext.queryTags(inputQueryItem);
Map<String, Object> salesItemQueryResult = (Map<String, Object>) salesItemQueryOutput.get('queryResult');
List<Object> salesItemDataList = (List<Object>) salesItemQueryResult.get('SalesTransactionItem');
System.debug(DEBUG_VERSION + ' - SalesTransactionItem Raw Query: ' + JSON.serializePretty(salesItemQueryResult));
if (salesItemDataList != null) {
for (Object itemObj : salesItemDataList) {
Map<String, Object> salesItemNode = (Map<String, Object>) itemObj;
List<Object> salesItemDataPathFull = (List<Object>) salesItemNode.get('dataPath');
Map<String, Object> salesItemTags = (Map<String, Object>) salesItemNode.get('tagValue');
if (salesItemDataPathFull == null || salesItemDataPathFull.size() < 3 || salesItemTags == null || !salesItemTags.containsKey('LineItem')) {
System.debug(DEBUG_VERSION + ' - Invalid dataPath or missing LineItem tag for a SalesTransactionItem node. Path: ' + JSON.serialize(salesItemDataPathFull));
continue;
}
// The dataPath for SalesTransactionItem is usually [ContextInstanceId, QuoteId, LineContextId]
// We need QuoteId and LineContextId for the nodePath update
List<Object> salesItemPathForNodeUpdate = new List<Object>{
salesItemDataPathFull.get(1), // QuoteId
salesItemDataPathFull.get(2) // LineContextId (0QL... or ref_...)
};
String currentItemQliIdOrRef = (String)((Map<String, Object>)salesItemTags.get('LineItem')).get('tagValue');
String lineItemPathTagValue = salesItemTags.containsKey('LineItemPath') ? (String)((Map<String, Object>)salesItemTags.get('LineItemPath')).get('tagValue') : currentItemQliIdOrRef;
// Determine the source QLI ID for attribute lookup (handles parent/child relationships)
String attrSourceQliId;
if (lineItemPathTagValue != null && lineItemPathTagValue.contains('/')) {
attrSourceQliId = lineItemPathTagValue.split('/')[0];
} else {
attrSourceQliId = currentItemQliIdOrRef;
}
if (qliToAttributeValues.containsKey(attrSourceQliId)) {
Map<String, String> itemAttributes = qliToAttributeValues.get(attrSourceQliId);
List<String> descriptionParts = new List<String>();
// Concatenate attribute values in the specified order
for (String attrName : ATTRIBUTES_FOR_DESCRIPTION) {
if (itemAttributes.containsKey(attrName) && itemAttributes.get(attrName) != null) {
descriptionParts.add(itemAttributes.get(attrName));
} else {
descriptionParts.add(''); // Add an empty string if an attribute is missing to maintain structure or a placeholder
System.debug(DEBUG_VERSION + ' - Attribute ' + attrName + ' not found or null for QLI: ' + attrSourceQliId);
}
}
String newDescription = String.join(descriptionParts, ' - '); // Join with a separator
if (String.isNotBlank(newDescription)) {
Map<String, Object> descriptionUpdateNode = new Map<String, Object>{
'nodePath' => new Map<String, Object>{'dataPath' => salesItemPathForNodeUpdate},
'attributes' => new List<Object>{
new Map<String, Object>{
'attributeName' => DESCRIPTION_FIELD_API_NAME,
'attributeValue' => newDescription
}
}
};
allNodeUpdates.add(descriptionUpdateNode);
System.debug(DEBUG_VERSION + ' - Queued description update for item (QLI Ref: ' + currentItemQliIdOrRef + ', Attr Source QLI: ' + attrSourceQliId + '). New Description: ' + newDescription + '. DataPath for Update: ' + JSON.serialize(salesItemPathForNodeUpdate));
} else {
System.debug(DEBUG_VERSION + ' - Generated description is blank for item (QLI Ref: ' + currentItemQliIdOrRef + ', Attr Source QLI: ' + attrSourceQliId + '). No update queued.');
}
} else {
System.debug(DEBUG_VERSION + ' - No attributes found for QLI: ' + attrSourceQliId + ' to build description for item (QLI Ref: ' + currentItemQliIdOrRef + ').');
}
}
}
// STEP 3: Submit context update if there are any changes
if (!allNodeUpdates.isEmpty()) {
Map<String, Object> updateInput = new Map<String, Object>{
'contextId' => contextId,
'nodePathAndAttributes' => allNodeUpdates
};
System.debug(DEBUG_VERSION + ' - Submitting Context Update: ' + JSON.serializePretty(updateInput));
industriesContext.updateContextAttributes(updateInput);
response.status = RevSignaling.TransactionStatus.SUCCESS;
response.message = 'Post-hook ' + DEBUG_VERSION + ': SalesTrxnItemDescription updated successfully.';
} else {
response.status = RevSignaling.TransactionStatus.SUCCESS;
response.message = 'Post-hook ' + DEBUG_VERSION + ': No description updates were necessary.';
System.debug(DEBUG_VERSION + ' - No updates to submit.');
}
} catch (Exception e) {
System.debug(DEBUG_VERSION + ' - ERROR during Post-hook execution: ' + e.getMessage() + ' Stack: ' + e.getStackTraceString());
response.status = RevSignaling.TransactionStatus.FAILED;
response.message = 'Post-hook ' + DEBUG_VERSION + ' Error: ' + e.getMessage();
// Optionally, rethrow a custom exception if needed for more specific error handling upstream
// throw new OtherException(response.message);
}
return response;
}
}