Skip to main content

Sample code for hooks and events

Zuora

Sample code for hooks and events

This article provides sample codes that use hooks and events in the extensibility framework.

Use async / await for long-running async JS tasks. 

Sample code 1

Following is a sample code that uses the following hooks and events:

  • beforeproductAdd
  • afterProductAdd
  • beforeProductUpdate
  • afterProductUpdate
  • beforeRulesExecution
  • afterRulesExecution
  • objectfieldconfig event
import {
   LightningElement,
   api
} from "lwc";

export default class ComponentHeadless extends LightningElement {
   prefix = "zqu__";
   applyConfigsForQuotes = true;

   @api quoteState;
   @api metricState;
   @api pageState;
   @api masterQuoteState;
   @api parentQuoteState;
   
   @api
   beforeProductAdd(ratePlanMap, effectiveDate) {
       if (ratePlanMap.hasOwnProperty("a0aRL0000084T2aYAK")) {
           return { // If the Product Rate Plan to be added is a0aRL0000084T2aYAK. Then stop product add
               proceed: false
           };
       }
       const ratePlanToAdd = "a0aRL000008Q3AaYAK"; //Product Rate Plan Id to add
       const quantity = 2; //Quantity of the above product rate plan to be added
       ratePlanMap = {
           ...ratePlanMap,
           [ratePlanToAdd]: quantity
       };
       let newEffectiveDate;
       if (
           this.quoteState.quote.zqu__Billing_Account_Name__c === "Test Account" &&
           effectiveDate === "2024-10-10"
       ) { // Change the effective date based on conditions
           newEffectiveDate = "2024-10-20"; //Optional
       }

       this.dispatchEvent(
           new CustomEvent("toastmessagedisplay", {
               detail: {
                   message: "From beforeProductAdd",
                   theme: "success",
               },
           })
       );
       if (!newEffectiveDate) { // Effective Date prop is optional, if not provided, the existing effective date will be considered
           return {
               proceed: true,
               newRatePlanMap: ratePlanMap
           };
       }
       return {
           proceed: true,
           newRatePlanMap: ratePlanMap,
           effectiveDate: newEffectiveDate,
       };
   }
   
   @api
   afterProductAdd(newProductTimeline) {
       this.processProductTimelines(newProductTimeline, false);
   }
   
   @api
   beforeProductUpdate(
       updatedCharges,
       updatedAmendment,
       updatedRatePlan,
       isRevert
   ) {
       if (updatedCharges && !(updatedAmendment && updatedRatePlan)) {
           //Quote Rate Plan Charge update
           if (updatedCharges[0].Name === "Economy") {
               updatedCharges[0].zqu__Quantity__c = 5; // Update the quantity field of the charge based on condition
               return {
                   proceed: true,
                   updatedCharges
               };
           } else if (updatedCharges[0].Name === "Standard") {
               return { // Stop the Charge Update
                   proceed: false
               };
           }
           return { // Proceed with the Charge Update If no changes are required
               proceed: true,
               updatedCharges
           };
       } else if (updatedAmendment && !(updatedCharges && updatedRatePlan)) {
           // Quote Amendment update
           // Quote Amendment record is present in updatedAmendment.amendment only for Amendment update
           if (
               updatedAmendment.amendment.zqu__ContractEffectiveDate__c ===
               "2024-10-10"
           ) {
               // update the Quote Amendment fields based on conditions
               updatedAmendment.amendment.zqu__ContractEffectiveDate__c = "2024-10-20";
               updatedAmendment.amendment.zqu__CustomerAcceptanceDate__c =
                   "2024-10-20";
               updatedAmendment.amendment.zqu__ServiceActivationDate__c = "2024-10-20";
           } else if (updatedAmendment.specificUpdateDate === "2024-10-12") {
               // specificUpdateDate will be present in the updatedAmendment object only if there is an update on that property in the order Action Modal
               updatedAmendment.specificUpdateDate === "2024-11-20"; // Update the specificUpdateDate based on conditions
           } else if (
               updatedAmendment.amendment.zqu__ContractEffectiveDate__c ===
               "2024-10-10"
           ) {
               return { // Stop the Quote Amendment update
                   proceed: false
               };
           }
           return { // Proceed with the Amendment field Update If no changes are required
               proceed: true,
               updatedAmendment
           };
       } else if (updatedRatePlan && !(updatedCharges && updatedAmendment)) {
           //Quote Rate Plan custom fields update
           if (updatedRatePlan.Name === "Recurring Fee") {
               updatedRatePlan.zqu__Custom_Field = 20; // Update Quote Rate Plan Custom Fields based on conditions
               return {
                   proceed: true,
                   updatedRatePlan
               };
           } else if (updatedCharges[0].Name === "Flat Fee") {
               return { // Stop Quote Rate Plan Custom Field Update
                   proceed: false
               };
           }
           return {
               proceed: true,
               updatedRatePlan
           };
       } else if (
           updatedCharges &&
           updatedAmendment &&
           updatedRatePlan &&
           !isRevert
       ) {
           //Future Dated Update
           if (updatedRatePlan.Name === "Standard Plan") { // Update Quote Rate Plan, Quote Amendment and Charge fields based on conditions if it is a Future Dated Update
               updatedRatePlan.zqu__Custom_Field = 11;
               updatedAmendment.zqu__ContractEffectiveDate__c = "2024-10-11";
               updatedCharges[0].zqu_Quantity__c = 10;
           }
           return {
               proceed: true,
               updatedRatePlan,
               updatedCharges,
               updatedAmendment,
           };
       } else if (
           updatedCharges &&
           updatedAmendment &&
           updatedRatePlan &&
           isRevert
       ) {
           // Future Dated Update revert or Future Dated Remove revert
           return {
               proceed: true
           };
       }
   }
   
   @api
   afterProductUpdate(updatedCharges, updatedRatePlan) {
       let stopLoop = false;
       let configs = [];
       const timelines = this.quoteState.subscription.productTimelines;
       for (const [, timeline] of Object.entries(timelines)) {
           if (stopLoop) break;
           for (const [index, version] of timeline.versions.entries()) {
               if (stopLoop) break;


               const nextEffectiveDate =
                   timeline.versions[index + 1]?.amendment.record[
                       this.prefix + "ContractEffectiveDate__c"
                   ];
               const currentEffectiveDate =
                   version.amendment.record[this.prefix + "ContractEffectiveDate__c"];
               if (
                   !timeline.versions[index + 1]?.voidAction &&
                   currentEffectiveDate === nextEffectiveDate
               ) // Skip if the effective date is same as the next version (Scenario: While amending the original charge on the same date)
                   continue;


               for (const [chargeIndex, charge] of version.charges.entries()) {
                   if (
                       updatedRatePlan[0].Id === version.Id &&
                       (!updatedCharges ||
                           updatedCharges.find(
                               (updatedCharge) =>
                               updatedCharge[this.prefix + "ProductRatePlanCharge__c"] ===
                               charge.record[this.prefix + "ProductRatePlanCharge__c"]
                           ))
                   ) {
                       const quantity = charge.record[this.prefix + "Quantity__c"];
                       configs.push(
                           ...this.generateConfigsForCharges( //Generate configs for the charge fields based on the change in quantity field
                               chargeIndex,
                               timeline.timelineId,
                               version.amendment.record[
                                   this.prefix + "ContractEffectiveDate__c"
                               ],
                               quantity
                           )
                       );
                       if (quantity >= = 10) {
                           configs.push({ // Apply configs for custom Quote Rate Plan fields in the Quote Rate Plan Modal
                               field: this.prefix + "Custom_Field__c",
                               effectiveDate: currentEffectiveDate,
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteRatePlan__c",
                               backgroundColor: "orange",
                               readOnly: false,
                           });
                           configs.push({ // Apply configs for Quote Amendment fields in the Order Action Detail Modal
                               field: this.prefix + "ContractEffectiveDate__c",
                               effectiveDate: currentEffectiveDate,
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteAmendment__c",
                               backgroundColor: "blue",
                               helpText: "Quanity cannot be greater than 9",
                               disabled: true,
                           });
                           configs.push({ // Apply configs for Quote Amendment fields in the Future Dated Update Modal
                               field: this.prefix + "ContractEffectiveDate__c",
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteAmendment__c",
                               hidden: true
                           });
                       } else {
                           configs.push({
                               field: this.prefix + "Custom_Field__c",
                               effectiveDate: currentEffectiveDate,
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteRatePlan__c",
                               backgroundColor: "default",
                               readOnly: true,
                           });
                           configs.push({
                               field: this.prefix + "ContractEffectiveDate__c",
                               effectiveDate: currentEffectiveDate,
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteAmendment__c",
                               backgroundColor: "default",
                               helpText: null,
                               disabled: false,
                           });
                           configs.push({
                               field: this.prefix + "ContractEffectiveDate__c",
                               timelineId: timeline.timelineId,
                               object: this.prefix + "QuoteAmendment__c",
                               hidden: false
                           });;
                           stopLoop = true; //Stop the loop after the execution is completed for the particular charge update
                       }
                   }
               }
           }

           if (configs.length > 0) {
               this.dispatchEvent(
                   new CustomEvent("objectfieldconfig", {
                       detail: {
                           configs
                       }
                   })
               );
           }
       }
   }
   
   @api
   async beforeRulesExecution(context) {
       console.log(`Running beforeRulesExecution in context : ${context.triggerEvent}`);
       const rulesIdsToExecute = this.pageState.rules?.active.filter(rule => rule.name === 'Test Rule')?.map(fiteredRule => fiteredRule.Id); //Filter the rule Ids based on criteria
       return {
           rulesToExecute: rulesIdsToExecute,
           runRules: true
       }
   }
   
   @api
   afterRulesExecution(rulesResponse) {
       if (
           rulesResponse.newlyAddedTimelines &&
           Object.keys(rulesResponse.newlyAddedTimelines).length > 0
       ) // If an new product is added through rules engine
           this.processProductTimelines(rulesResponse.newlyAddedTimelines);


       const updatedProductTimelines =
           rulesResponse.updatedQuoteState.subscription.productTimelines;
       if (updatedProductTimelines) // If there are changes in the product timelines
           this.processProductTimelines(updatedProductTimelines, true);
   }

   /* Generates config for charges based on quantity change and returns back the JS object of config */
   generateConfigsForCharges(chargeIndex, timelineId, effectiveDate, quantity) {
       const baseConfig = {
           chargeIndex,
           timelineId,
           effectiveDate,
           object: this.prefix + "QuoteRatePlanCharge__c",
       };

       const customConfig = {
           ...baseConfig,
           field: "Custom_Field__c"
       };
       const discountConfig = {
           ...baseConfig,
           field: this.prefix + "Discount__c",
           readOnly: false,
           disabled: false,
           hidden: false,
       };
       if (quantity === 1) {
           customConfig.backgroundColor = "red";
           customConfig.helpText = "Quantity is 1";
       } else if (quantity >= 2 && quantity <= 5) {
           customConfig.backgroundColor = "#ff6347";
           customConfig.helpText = "Quantity is between 2 and 5";
           discountConfig.hidden = true;
       } else if (quantity >= 6 && quantity <= 10) {
           customConfig.backgroundColor = "rgba(255, 0, 191, 0.5)";
           customConfig.helpText = "Quantity is between 6 and 10";
           discountConfig.disabled = true;
       } else if (quantity >= 11 && quantity <= 15) {
           customConfig.backgroundColor = "hsl(105, 100%, 50%)";
           customConfig.helpText = "Quantity is between 11 and 15";
           discountConfig.readOnly = true;
       } else {
           customConfig.backgroundColor = "default";
           customConfig.helpText = null;
       }
       return [customConfig, discountConfig];
   }

   /* Process the product timeline and fire the objectfieldconfig event */
   processProductTimelines(productTimelines, rulesExecuted) {
       const productTimelineEntries = Object.entries(productTimelines);
       if (!productTimelineEntries.length) return;
       let configs = [];
       for (const [, timeline] of productTimelineEntries) {
           for (const [index, version] of timeline.versions.entries()) {
               // Check if version is changed in the rules engine and voidAction (deleted rate plan) is not true
               if (
                   !version.voidAction &&
                   (!rulesExecuted || version.isChangedInRulesEngine)
               ) {
                   const currentEffectiveDate =
                       version.amendment.record[this.prefix + "ContractEffectiveDate__c"];
                   const nextEffectiveDate =
                       timeline.versions[index + 1]?.amendment.record[
                           this.prefix + "ContractEffectiveDate__c"
                       ];
                   if (currentEffectiveDate === nextEffectiveDate) continue; // Skip if the effective date is same as the next version (Scenario: While amending the original charge on the same date)
                   for (const [chargeIndex, charge] of version.charges.entries()) {
                       if (
                           !rulesExecuted ||
                           charge.fieldsChangedInRulesEngine?.length > 0
                       ) {
                           const quantity = charge.record[this.prefix + "Quantity__c"];
                           configs.push(
                               ...this.generateConfigs(
                                   chargeIndex,
                                   timeline.timelineId,
                                   currentEffectiveDate,
                                   quantity
                               )
                           );
                       }
                   }
               }
           }
       }
       if (this.applyConfigsForQuotes) {
           if (this.quoteState?.quote) { // Apply configs for the quote
               configs.push({
                   field: this.prefix + 'ValidUntil__c',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.quoteState.quote.Id,
                   backgroundColor: 'blue',
                   helpText: 'Fill the Valid Till Date for the quote',
               });
               configs.push({
                   field: this.prefix + 'InitialTerm__c',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.quoteState.quote.Id,
                   readOnly: true,
                   helpText: 'Default Initial term cannot be changed'
               });
               configs.push({
                   field: 'Name',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.quoteState.quote.Id,
                   backgroundColor: 'red',
                   helpText: 'Fill the Quote Name',
               });
               configs.push({
                   field: this.prefix + 'Opportunity__c',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.quoteState.quote.Id,
                   hidden: true
               });
           }
           if (this.parentQuoteState?.quote) { // Applying configs for parent quote in the MSQ context
               configs.push({
                   field: 'Name',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.parentQuoteState.quote.Id,
                   backgroundColor: 'pink'
               });
               configs.push({
                   field: 'Name',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.parentQuoteState.quote.Id,
                   helpText: 'Fill the Parent Quote Name',
                   hidden: true
               });
               configs.push({
                   field: this.prefix + 'Opportunity__c',
                   object: this.prefix + 'Quote__c',
                   quoteId: this.parentQuoteState.quote.Id,
                   backgroundColor: 'violet',
                   helpText: 'Fill the opportunity Name',
                   hidden: false
               });
           }
           this.applyConfigsForQuotes = false;
       }
       if (configs.length > 0) {
           this.dispatchEvent(
               new CustomEvent("objectfieldconfig", {
                   detail: {
                       configs
                   }
               })
           );
           this.executed = true;
       }
   }
}

Sample code 2

Following is the sample code to auto-populate billing language picklist values when the Sold To Contact field is modified.

import { LightningElement, api } from 'lwc';

const PREFIX = 'zqci15__';
export default class HeadLessComp extends LightningElement {
    @api pageState;
    _previousQuoteState;
    _currentQuoteState;

    @api metricState;

    connectedCallback() {
        console.log('Component Loaded...');
    }

    @api
    get quoteState() {
        return this._currentQuoteState;
    }

    set quoteState(newState) {
        this._currentQuoteState = newState;

        // Destructure quotes for easier access
        const newQuote = newState?.quote;
        const oldQuote = this._previousQuoteState?.quote;

        if (oldQuote && this.hasFieldChanged(oldQuote, newQuote, 'SoldToContact__c')) {
            const updatedQuote = { ...newQuote, [`${PREFIX}Custom_Picklist__c`]: 'Value2' };

            // Store the current state and dispatch the event
            this._previousQuoteState = { ...newState };
            this.dispatchQuoteUpdate(updatedQuote);
        } else {
            // Simply store the current state if no changes detected
            this._previousQuoteState = { ...newState };
        }
    }

    hasFieldChanged(oldQuote, newQuote, fieldName) {
        return newQuote?.[`${PREFIX}${fieldName}`] && oldQuote?.[`${PREFIX}${fieldName}`] !== newQuote?.[`${PREFIX}${fieldName}`];
    }

    dispatchQuoteUpdate(updatedQuote) {
        this.dispatchEvent(new CustomEvent('updatequote', {
            detail: { quote: updatedQuote }
        }));
    }
}

Sample code 3

The following is the sample code to change the quote name before saving execution. This example uses a hook before saving and an event before the preview call.

import { LightningElement, api } from 'lwc';
export default class HeadLessComp extends LightningElement {
@api quoteState;
@api metricState;
@api pageState;
sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
connectedCallback() {
console.log('Custom Component is Loaded,..');
this.dispatchEvent(new CustomEvent('toastmessagedisplay', {
detail: {
message: 'Custom Component is Loaded...',
theme: 'success'
}
}));
}

@api
async beforeSave() {

//Use Async / Await to dely execution - If any long running task like external API Calls to be completed and if it's response has a dependency on the logics in this method
await this.sleep(5000);

// will be executed after 5000 ms
this.dispatchEvent(new CustomEvent('toastmessagedisplay', {
detail: {
message: 'Ssve logic from Custom Component',
theme: 'error'
}
}));

this.quoteState = JSON.parse(JSON.stringify(this.quoteState));
this.quoteState.quote.Name = 'Quote Name from Custom Comp';

this.dispatchEvent(new CustomEvent('updatequote', {
detail: { quote: this.quoteState.quote }
}));

//continue Quote Studio SAVE Logic
return true;
}

@api
beforePreviewCall() {

this.dispatchEvent(new CustomEvent('toastmessagedisplay', {
detail: {
message: 'Preview Call logic from Custom Component',
theme: 'success'
}
}));

//continue Quote Studio Preview on-demand Logic
return true;
}

@api
beforeSubmit() {

this.dispatchEvent(new CustomEvent('toastmessagedisplay', {
detail: {
message: 'Submit logic from Custom Component',
theme: 'error'
}
}));

//STOP Quote Studio SUBMIT Logic
return false;
}
}

Sample code 4

The following is a sample code for opensidebarcomponent, closesidebar and changesidebarwidth.

When the Save button is clicked, the sidebar component opens to allow input for the Initial Term value. After clicking the Save button in the custom component, the sidebar closes, and the quote is saved.

//commonUtils to hold the static value on whether beforeSave hook should execute or not
let _executeSaveLogic = true;
export const getExecuteSaveLogic = () => {
   return _executeSaveLogic;
};

export const setExecuteSaveLogic = (value) => {
   _executeSaveLogic = value;
};
// Custom sidebar component which takes the initial term value as input, updates the quote with new initial term and save the quote
import { LightningElement, api } from 'lwc';
import { setExecuteSaveLogic } from 'c/commonUtils';

export default class CustomSidebarComponent extends LightningElement {
   @api quoteState;
   @api metricState;
   @api pageState;
   @api masterQuoteState;
   @api parentQuoteState;

   initialTerm; // To store the Initial Term values
   handleClick() {
       this.dispatchEvent(new CustomEvent('closesidebar')); // Event to close the sidebar component


       let quoteClone = Object.assign({}, this.quoteState.quote);
       quoteClone.zqci15__InitialTerm__c = this.initialTerm;
       const updateQuoteEvent = new CustomEvent('updatequote', { // Update the quote with new initial term value
           detail: {
               quote: quoteClone
           }
       });
       this.dispatchEvent(updateQuoteEvent);
       setTimeout(() => { // Save the quote after update quote event is executed
           setExecuteSaveLogic(false); // Do not run the beforeSave hook
           this.dispatchEvent(new CustomEvent('savequote'));
       }, 0);
   }
   handleChange(event) {
       this.initialTerm = event.target.value; // To store the Initial Term value on change
   }
}
import { LightningElement, api } from 'lwc';
import { getExecuteSaveLogic } from 'c/commonUtils';

export default class ComponentHeadless extends LightningElement {
   @api quoteState;
   @api metricState;
   @api pageState;
   @api masterQuoteState;
   @api parentQuoteState;
   connectedCallback() {
       this.dispatchEvent(
           new CustomEvent('changesidebarwidth', { detail: { width: '80vw' } }) // Event to change the sidebar width
       );
   }

   @api
   beforeSave() {
       if (getExecuteSaveLogic()) { // Check whether to execute the logic in beforeSave hook
           this.dispatchEvent(
               new CustomEvent('opensidebarcomponent', { // Open Sidebar component
                   detail: {
                       componentName: 'componentHead'
                   }
               })
           );
           return false;
       }
       return true;
   }
}