Apex Class for Prehook and Posthook examples

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 dmlStatus of 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 dataPath for updating both SalesTransactionItemAttribute nodes (the attributes themselves) and SalesTransactionItem nodes (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;
    }
}
Free Assessment