Pricing Engine Plugins for Change Quotes and Orders
51 min
change order pricing engine plugins change order plugins let you customize how prices are calculated when an existing subscription is renewed, amended, upgraded, or cancelled instead of relying solely on price book entries and discount tags configured in the catalog, you can inject business logic — such as honoring a contractually negotiated rate, stripping a promotional discount at renewal time, or capping a price regardless of what the catalog says plugins run on the server as part of the pricing engine during changeorder() and lifecycle change api calls they do not run during createquote() or createorder() — those operations use standard pricing engine plugins with plugintype = 'any' or 'headerobject' availability change order plugins require the changeorder plugin type, available in release 2603 how plugins fit into the pricing pipeline every change order passes through a two stage pipeline before prices are finalized you can hook into either stage — or both — depending on whether you want to modify the inputs before calculation or override the outputs after trigger event when it runs what you can do beforecalculation before the pricing engine calculates totals modify inputs override net sales price, inject or remove discount tags, adjust list price aftercalculation after the pricing engine has calculated totals override outputs cap total price, set discount percentage, override net sales price on results the two events form a true pipeline an aftercalculation plugin sees the results of any beforecalculation changes for example, if a beforecalculation plugin injects a 10% discount tag, the engine calculates the discounted price, and the aftercalculation plugin receives that discounted subtotal — not the original list price change order pricing plugin creating a plugin record a plugin is a ruby pricingplugin c salesforce record that contains the javascript code you want to run, along with metadata that tells the engine when to run it and for which operations setting ruby plugintype c to changeorder ensures the plugin runs only during change order processing and is ignored by quote and order creation fields field type required description name text yes display name for the plugin ruby code c long text yes javascript code (es5 syntax) ruby triggerevent c picklist yes beforecalculation or aftercalculation ruby plugintype c picklist yes must be changeorder ruby isactive c checkbox yes set to true to enable the plugin ruby ecma version c text no ecmascript version (default "9" ) ruby apiversion c text no api version (default "latest" ) apex example ruby pricingplugin c plugin = new ruby pricingplugin c( name = 'renewal price override', ruby code c = 'var lineitems = $$headerobject lineitems || \[];\n' \+ 'for (var i = 0; i < lineitems length; i++) {\n' \+ ' $$updatedlineitems push({ id lineitems\[i] id, netsalesprice 42 });\n' \+ '}', ruby triggerevent c = 'beforecalculation', ruby plugintype c = 'changeorder', ruby ecma version c = '9', ruby isactive c = true, ruby apiversion c = 'latest' ); insert plugin; syntax requirements plugin code must be written in es5 javascript — the sandbox environment does not support modern syntax this means no const or let (use var instead), no arrow functions ( => ) (use named function declarations), and no template literals ( ` ) (use string concatenation) code also cannot contain top level return statements, because it runs as a script rather than a function body plugins execute in a sandboxed javascript environment on the pricing engine server, isolated from other salesforce code what the plugin receives when your plugin runs, the pricing engine injects a global object called $$headerobject this object contains all the line items involved in the change order, along with the pricing data and change instructions associated with each one your plugin reads from $$headerobject to understand the current state, then writes to an output variable ( $$updatedlineitems or $$updatedlineitemprices ) to tell the engine what to change $$headerobject lineitems\[] each entry in lineitems represents one product line in the change order field type description id string unique line item identifier you must include this in any output object to tell the engine which line you are modifying quantity number current quantity netsalesprice number current net sales price (unit price) listprice number list price from the price book pricetags array price and discount dimensions attached to this item (see below) childrenlineitems array nested child line items for bundles (hierarchical, not flattened) changerequests array the change operations requested for this line item (see below) product sku string product sku product pricemodel string pricing model (e g , "perunit" , "flatfee" ) a common point of confusion changetype is not on lineitems the context line items do not have changetype or linetype as direct fields if you need to know whether a line is being renewed, cancelled, or otherwise changed, you must look at the changerequests array on that line item the changeitems array — which does contain changetype — is only available in aftercalculation changerequests\[] each line item carries a changerequests array that describes what the api was asked to do with that line a single line item can have more than one change request — for example, a simultaneous renewal and quantity update field type description changetype string the type of operation being performed "renew" , "updatequantity" , "updateterm" , "cancel" , "upgrade" , "downgrade" , or "adjustprice" renewalterm number the length of the renewal term, in the subscription's term dimension (for renew operations) term number the term change value (for updateterm operations) quantity number the delta quantity — positive for increases, negative for decreases (for updatequantity operations) startdate string the effective date of the change (iso yyyy mm dd format) pricetags\[] price tags are the discount and pricing dimensions attached to a line item your plugin can read the existing tags, modify the list, and return the updated set to change how the engine prices the item field type description recordtype string "pricedimension" for pricing rules or "discountdimension" for discounts pricedimensiontype string dimension type (e g , "quantity" ) pricetype string price type (e g , "tiered" , "volume" ) active boolean whether the tag is currently active pricetiers array tier definitions with tiernumber , startunit , chargemodel , discountpercentage , etc bundle products when the change order involves a bundle, the parent line item appears in $$headerobject lineitems\[] with a childrenlineitems array containing each component product each child has the same shape as a top level line item — it has its own id , product sku , pricetags , and so on to modify a child's price, target the child's id in your output, not the parent's var children = lineitems\[i] childrenlineitems || \[]; for (var j = 0; j < children length; j++) { // children\[j] id, children\[j] product sku, etc } examples override net sales price on renewal use case your customer has a negotiated contract rate that differs from your standard price book when their subscription renews, you want the renewal priced at the agreed upon rate — $42 per unit — regardless of what the price book says this is common for enterprise customers with custom pricing agreements trigger beforecalculation context fields used lineitems\[] id output $$updatedlineitems with netsalesprice var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { $$updatedlineitems push({ id lineitems\[i] id, netsalesprice 42 }); } what you'll see every line item in the change order receives a net sales price of $42 per unit the engine then calculates the total price by multiplying the net sales price by quantity and term — for example, $42 × 5 seats × 1 year = $210 delta metrics such as deltaarr and deltatcv are recalculated automatically from the new values cap total price use case you want to guarantee that no single line item on a change order exceeds a maximum total value of $1,000 — regardless of quantity, term, or price book entry this is useful for promotional campaigns or budgetary commitments where a ceiling price has been agreed with the customer trigger aftercalculation context fields used lineitems\[] changeitems\[] output $$updatedlineitemprices with changeitems array var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var li = lineitems\[i]; if (li changeitems && li changeitems length > 0) { var updatedchangeitems = \[]; for (var j = 0; j < li changeitems length; j++) { updatedchangeitems push({ totalprice 1000 }); } $$updatedlineitemprices push({ id li id, changeitems updatedchangeitems }); } } what you'll see every change item on every line is capped at a total price of $1,000 the engine then recalculates netsalesprice and delta metrics to be consistent with the overridden total because this plugin runs after calculation, it overrides whatever price the engine arrived at — even if the natural price would have been lower add a volume discount tag use case you want to automatically apply a 10% discount to all line items when a customer renews, as a loyalty incentive — without baking the discount permanently into the price book using a plugin keeps the discount separate from the catalog configuration and easy to adjust or remove at any time trigger beforecalculation context fields used lineitems\[] pricetags output $$updatedlineitems with pricetags (or pricedimensions — both work) var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var li = lineitems\[i]; var tags = (li pricetags || \[]) slice(); tags push({ recordtype "discountdimension", pricedimensiontype "quantity", pricetype "tiered", active true, pricetiers \[{ tiernumber 1, startunit 0, chargemodel "perunit", discountpercentage 10 }] }); $$updatedlineitems push({ id li id, pricetags tags }); } what you'll see the 10% discount is applied during calculation, exactly as if it had been configured on the price book entry in the resulting order items, systemdiscountamount will be greater than zero, and subtotal will be less than listtotalprice by the discount amount note you read from pricetags (the input field) but can write to either pricetags or pricedimensions on the output — both field names are accepted always call slice() to copy the array before modifying it; mutating the original input array can cause unexpected behavior target bundle child pricing use case your bundle contains several component products, but you want to adjust the price of just one specific component — for example, to give a customer a reduced rate on a particular add on at renewal — without changing the price of the bundle parent or any other components trigger beforecalculation context fields used lineitems\[] childrenlineitems\[] , product sku var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var children = lineitems\[i] childrenlineitems || \[]; for (var j = 0; j < children length; j++) { if (children\[j] product && children\[j] product sku && children\[j] product sku indexof("my product sku") === 0) { $$updatedlineitems push({ id children\[j] id, netsalesprice 1 }); } } } what you'll see only the matching child product receives the price override the bundle parent and all other child products retain their original prices notice that you must use children\[j] id — the child's own line item id — when writing to $$updatedlineitems using the parent's id would modify the parent instead conditional logic by change type use case you have a pricing rule that should apply only during renewals — not during amendments, cancellations, or upgrades rather than creating separate plugins for each operation type, you can add a condition inside a single plugin that checks what type of change is being processed and skips lines that don't match trigger beforecalculation context fields used lineitems\[] changerequests\[] changetype var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var li = lineitems\[i]; var cr = li changerequests || \[]; for (var j = 0; j < cr length; j++) { if (cr\[j] changetype === "renew") { $$updatedlineitems push({ id li id, netsalesprice 77 }); } } } what you'll see only line items that include a renew change request receive the price override lines being cancelled, amended, or upgraded pass through unchanged, using the prices determined by the standard catalog and any other active plugins remove promotional discount on renewal use case a customer was originally sold at a discounted rate, and that discount is stored as a discountdimension price tag on their subscription at renewal time, you want to remove the discount so the customer renews at the full list price — a common scenario when an introductory or promotional offer is set to expire trigger beforecalculation context fields used lineitems\[] pricetags\[] recordtype var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var li = lineitems\[i]; var tags = (li pricetags || \[]) filter(function(t) { return t recordtype !== "discountdimension"; }); $$updatedlineitems push({ id li id, pricetags tags }); } what you'll see the engine receives the line items without any discount dimensions, so it calculates at full list price in the resulting order items, systemdiscountamount will be zero and subtotal will equal listtotalprice override discount percentage use case after the engine has calculated prices, you want to apply a uniform 25% discount across all change order items — for example, as part of a retention offer agreed during a renewal negotiation using aftercalculation lets you apply the discount on top of whatever the engine calculated, without restructuring the input price tags trigger aftercalculation context fields used lineitems\[] changeitems\[] output $$updatedlineitemprices with discountpercentage in changeitems var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { var li = lineitems\[i]; if (li changeitems && li changeitems length > 0) { var updatedchangeitems = \[]; for (var j = 0; j < li changeitems length; j++) { updatedchangeitems push({ discountpercentage 25 }); } $$updatedlineitemprices push({ id li id, changeitems updatedchangeitems }); } } what you'll see each change item receives a 25% discount the engine recalculates totalprice as subtotal × (1 − 0 25) , and delta metrics are updated automatically to reflect the discounted total multiple plugins you can have any number of active change order plugins when multiple plugins are active, they run in the order the records were inserted into salesforce beforecalculation sequential chaining when multiple beforecalculation plugins are active, they run one after another each plugin sees the output of the previous one — meaning if plugin a modifies a field, plugin b will read the value that plugin a wrote, not the original catalog value if two plugins both set the same field, the value from the last plugin to run is the one the engine uses plugin a sets nsp=80 → plugin b sees nsp=80, sets nsp=60 → engine uses nsp=60 aftercalculation no chaining (last writer wins) aftercalculation plugins do not chain in the same way each aftercalculation plugin independently reads from the original calculated result — not from the output of any other aftercalculation plugin that ran before it if two plugins both override the same field, the one that ran last wins plugin a sets totalprice=800 → plugin b sees original total, sets discount=50% result discount=50% wins (plugin b ran last) beforecalculation and aftercalculation together when you have plugins at both events, they form a true pipeline all beforecalculation plugins run first, then the engine calculates, then all aftercalculation plugins run against the calculated result an aftercalculation plugin sees the prices that emerged from the beforecalculation phase — not the original catalog prices beforecalc adds 10% discount tag → engine calculates with discount → aftercalc sees discounted subtotal error handling and debugging plugin errors if your plugin throws a javascript error, the pricing engine surfaces it as a pricingengineexception that includes the plugin's name the change order api call fails and returns an error — the exception is not silently swallowed this means a broken plugin will block all change orders from processing until the plugin is fixed or deactivated // this will surface as pricingengineexception plugin validation failed throw new error("plugin validation failed"); debugging with console debug you can use console debug() to write diagnostic output from inside your plugin these messages appear in the pricing engine logs and are useful for verifying that the plugin is receiving the data you expect var lineitems = $$headerobject lineitems || \[]; console debug("plugin received " + lineitems length + " line items"); for (var i = 0; i < lineitems length; i++) { console debug("item " + i + " sku=" + (lineitems\[i] product || {}) sku \+ " qty=" + lineitems\[i] quantity); } defensive coding not every line item will have every field populated for example, a product with no bundle children will have childrenlineitems as null, and a line item with no discount tags will have pricetags as null always use a fallback to an empty array when reading these fields to avoid null reference errors that would fail the entire change order var lineitems = $$headerobject lineitems || \[]; // guard null lineitems var children = lineitems\[i] childrenlineitems || \[]; // guard null children var tags = (li pricetags || \[]) slice(); // guard null + copy before mutate var cr = li changerequests || \[]; // guard null changerequests api reference beforecalculation output ( $$updatedlineitems ) to tell the engine to change a line item's pricing inputs, push an object to $$updatedlineitems each object must include the id of the line item you are targeting, taken from $$headerobject lineitems\[i] id you only need to include the fields you want to change — any field you omit is left at its current value field type description id string required line item id from $$headerobject lineitems\[i] id netsalesprice number override the unit price listprice number override the list price pricetags array replace price/discount dimensions (canonical name) pricedimensions array replace price/discount dimensions (legacy name — both work) aftercalculation output ( $$updatedlineitemprices ) to override calculated results after the engine has run, push an object to $$updatedlineitemprices the key difference from beforecalculation output is that you must wrap your overrides in a changeitems array rather than setting flat fields each entry in changeitems corresponds to one change item on the line (positional — the first entry maps to the first change item) field type description id string required line item id from $$headerobject lineitems\[i] id changeitems array required array of overrides, one per change item (positional) changeitems\[] override fields field type description totalprice number override the calculated total price netsalesprice number override the net sales price discountpercentage number override the discount percentage (0–100) discountamount number override the discount amount override priority (highest to lowest) netsalesprice > discountpercentage > discountamount > totalprice when you set multiple fields on the same change item, the engine applies the highest priority one and recalculates the others from it delta metrics ( deltaarr , deltatcv , deltaacv , deltacmrr ) are automatically recalculated after any override you do not need to set them manually bundle change orders when a bundle is renewed or amended, the pricing engine provides both the parent and child products together in the context object understanding how they are represented helps you write plugins that target the right items parent line item appears as a top level entry in $$headerobject lineitems\[] , with a childrenlineitems array containing each component product child line items nested inside the parent's childrenlineitems array each child has its own id , product sku , and pricetags , and must be targeted by its own id in any output object summary item a read only aggregate line that rolls up totals for display purposes do not include it in your plugin output targeting parent vs child the following shows how to modify the parent's price and a specific child's price in the same plugin var lineitems = $$headerobject lineitems || \[]; for (var i = 0; i < lineitems length; i++) { // this targets the parent $$updatedlineitems push({ id lineitems\[i] id, netsalesprice 100 }); // this targets a specific child var children = lineitems\[i] childrenlineitems || \[]; for (var j = 0; j < children length; j++) { if (children\[j] product && children\[j] product sku === "child sku") { $$updatedlineitems push({ id children\[j] id, netsalesprice 50 }); } } } in aftercalculation results, changeitems on the parent reflect the parent's calculated totals child products appear as separate orderitem records in salesforce, with parentid c pointing back to the parent common mistakes 1\ using flat fields in aftercalculation output in beforecalculation, you write flat fields like netsalesprice directly on the output object in aftercalculation, flat fields are silently ignored — you must wrap your overrides in a changeitems array this is the most frequent source of confusion when switching between the two trigger events // wrong — flat fields are silently ignored in aftercalculation $$updatedlineitemprices push({ id li id, totalprice 1000 }); // correct — must use changeitems array $$updatedlineitemprices push({ id li id, changeitems \[{ totalprice 1000 }] }); 2\ reading changetype directly from lineitems changetype does not exist as a direct field on a line item it lives inside the changerequests array checking lineitems\[i] changetype will always return undefined , so any condition based on it will silently never match // wrong — lineitems don't have changetype if (lineitems\[i] changetype === "renew") { } // correct — read from changerequests var cr = lineitems\[i] changerequests || \[]; for (var j = 0; j < cr length; j++) { if (cr\[j] changetype === "renew") { } } 3\ using $$updatedlineitems in an aftercalculation plugin each trigger event uses a different output variable $$updatedlineitems is the output for beforecalculation; aftercalculation uses $$updatedlineitemprices writing to $$updatedlineitems inside an aftercalculation plugin has no effect and produces no error — the changes are simply ignored // wrong — beforecalculation output variable $$updatedlineitems push({ id li id, netsalesprice 50 }); // correct — aftercalculation uses $$updatedlineitemprices $$updatedlineitemprices push({ id li id, changeitems \[{ netsalesprice 50 }] }); 4\ expecting aftercalculation plugins to chain if you have two aftercalculation plugins, each one independently reads the original calculated result — not the output of the other you cannot build a chain where plugin b refines what plugin a produced if both plugins write the same field, only the last one to run takes effect // if plugin a sets totalprice=800 and plugin b sets discountpercentage=50 // plugin b does not see plugin a's totalprice=800 // plugin b sees the original calculated totalprice // last writer wins discountpercentage=50 is the final result 5\ mutating input arrays the pricetags array on a line item is a reference to the engine's internal data structure pushing to it or modifying it directly can cause unpredictable behavior in subsequent plugins or in the engine itself always create a copy with slice() before making changes // wrong — mutating the original array li pricetags push(newtag); // correct — copy first, then modify var tags = (li pricetags || \[]) slice(); tags push(newtag); $$updatedlineitems push({ id li id, pricetags tags }); 6\ using es6+ syntax the plugin sandbox runs es5 javascript using modern syntax like const , let , arrow functions, or template literals will cause a parse error and prevent the plugin from running at all the failure is not graceful — the entire change order call will fail // wrong — const, let, arrow functions, template literals const items = lineitems map(li => `sku ${li product sku}`); // correct — var, function keyword, string concatenation var items = \[]; for (var i = 0; i < lineitems length; i++) { items push("sku " + lineitems\[i] product sku); }
Have a question?
Get answers fast with Nue’s intelligent AI, expert support team, and a growing community of users - all here to help you succeed.
To ask a question or participate in discussions, you'll need to authenticate first.