Skip to main content

Enhancing the pricing Data Model

4 Tasks

50 mins

Visible to: All users
Advanced Pega Platform 8.8 English
Verify the version tags to ensure you are consuming the intended content or, complete the latest version.

Scenario

FSG initially used Rule-Declare-Expressions and decision tables to price and compute the cost of different priceable items, such as the event and optional hotel service, to support the event quoting process. The number of Rule-Declare-Expression was considered excessive and can increase when other priceable items are added, such a charging for parking cars and potentially offering shuttle service

FSG decided to implement a pricing model approach similar to what is used in eCommerce, or “shopping cart” software. In this approach, a Pricing data type was defined and contains the fundamental properties to compute prices based on the priceable’s pricing model. The two most basic pricing models are unit and volume.

FSG implemented Rule-Declare-OnChange rules that react to changes to the inputs to the Pricing data type, eliminating Rule-Declare-Expression. The solution still depended on decision tables. Before eliminating the Rule-Declare-Expression, there was the problem of having pricing model-specific rules at FSG-Data-Pricing class level. A better design was to use pattern inheritance and polymorphism to move pricing model-specific rules to their class (for example, FSG-Data-Pricing-Volume). The use of circumstancing as a specialization approach based on the pricing model was rejected early on. The option to use circumstancing for a fine-grained purpose at the FSG-Data-Pricing level, such as the difference between how a price and a cost are computed, was left open.

One reason for wanting to eliminate decision tables was that, to add a new priceable item, the decision table that specifies a priceable’s pricing model requires modification. The decision table that defines how to price the priceable also requires modification; this violated the Open/Closed Principle. A way to specify that a different pricing model is used for cost compared to prices did not exist either. FSG COE did not want to add another decision table; just the opposite, FSG COE wanted to eliminate decision tables.

There was also the problem of passing the inputs to the FSG-Data-Pricing recalculate data transform, setting the value of a corresponding display property, and updating a running total for total price and the total cost to then compute potential profit. There was a unique Recalculate activity for each priceable (for example, RecalculateHotel), which also violated the Open/Closed Principle.

The FSG COE knows that going from the current state of their “pricing engine” to one that follows Open/Closed Principle requires analysis. That is where this challenge begins.

The following table provides the credentials you need to complete the challenge.

Role User name Password
Admin COE@FSG rules
Admin Admin@FSGPricingTest rules

 

You must initiate your own Pega instance to complete this Challenge.

Initialization may take up to 5 minutes so please be patient.

Detailed Tasks

1 Transition to Pega Live Data

FSF COE was aware that to prevent the overall solution from becoming overly complicated, it had to keep each part of the solution as simple as possible. Complexity increases with every variable that is added. The first step was to convert the PricingModel decision table to a Priceable data type. "Pricing Model" was not used as the data type name. The data type's purpose is not to encapsulate what a pricing model is. Instead, the data type's purpose is to encapsulate the item's ID to be priced and the pricing model to use to derive its price.

The best way to begin was to keep the definition of the FSG-Data-Priceable class as minimal as possible. The class could have a PricingType property, where the value of PricingType is either "Price" or "Cost." Doing so can result in multiple rows having the same ItemID, which adds complexity. The PricingType must be passed as a parameter for every rule where ItemID is passed. It makes more sense to have the Priceable data type have a single key, ItemID, where “Cost” is appended to value when the Priceable represents an internal cost as opposed to an advertised selling price.

The ItemID's value can indicate whether it represents a cost by appending "Cost" to its price ItemID counterpart. The VolumeIncludesQty property belongs in the Priceable class even though it only applies to the "Volume" pricing model. The property says, if true, the prices are as shown. Otherwise, if false, the price should be multiplied by the quantity used to derive the price.

Priceable

ItemID (key)

PricingModel

VolumeIncludesQty

The Pricing table already exists. Every column in this table is historical in nature. The table records what case owns the record (Reference), what the Priceable is, the computed Price, and the inputs that are used to compute the Price: Quantity, Bit, DiscountFactor, PricingModel, and if applicable, VolumeIncludesQty.

Pricing

ItemID (key)

Reference (key)

Price

Quantity

Bit

DiscountFactor

PricingModel (key)

VolumeIncludesQty

The decision table originally named Pricing was decomposed to a UnitPricing decision table within the FSG-Data-Pricing-Unit class and a VolumePricing decision table within the FSG-Data-Pricing-Value class. Polymorphism is used against a data transform named DerivePricing, which is stubbed out in FSG-Data-Pricing, then overridden in each derived class.

The first step is to move the columns in both decision table to a live data table. The name of the live data table cannot be Pricing because that name is already in use. A better name for the table is Price, because that is the information that the table contains.

Price

ItemID

Quantity From

Quantity To

PricingModel

Price

It is possible to say that a Price record has four keys, namely the four columns on the left. If only two keys (ItemID and PricingModel) are used, that says, “Every priceable can have only one row per PricingModel.” That does not make sense because Volume pricing itself requires multiple rows, one for each From/To quantity range. Trying to enforce that only one, non-overlapping, contiguous set of Volume quantity range rows is defined is not possible. Potential exists to add an AsOfDate column to this table to allow prices to transition to new values over time smoothly. Due to the complexity of unique key enforcement, the decision is made to auto-generate pyGUID as the primary key by checking the box on the FSG-Data-Price data type’s class rule.

2 Devise an Open/Closed solution

In Stage 1, devising an Open/Closed solution is straightforward: convert two decision tables into two live data tables. The next stage is more challenging.

The simplest task is finding an alternative to the InitPricing activity at the beginning of the Quotation stage. The activity is specific to the priceables that the Booking application quotes. A superior alternative is to find a way to initialize priceables for any application.

Before discussing this, one might ask, “Why is it necessary to initialize a set of Pricing records before quoting? Why not, instead, force the user to build a list of Pricing records using a Table layout, adding one row at a time?”.  The answer is that it is not necessary, but much more user-friendly and productive, if Pricing records are initialized. Suppose the user is forced to build the initial list of Pricing records themselves every time. In that case, they must remember which priceables to start with and whether they are required or optional. This could lead to errors. The salesperson should focus on communicating with the customer, not making unnecessary mouse clicks. There is an analogy to ordering food at a restaurant. It is much easier and expedient to have someone select from a menu than asking a waiter to recite from memory everything on the menu.

The inputs needed to calculate the Price property within a Pricing record are recorded. At the beginning, the DiscountFactor is set to 1, meaning no discount. This can be set in the FSG-Data-Pricing pyDefault data transform. If an item is always required, the Integer Bit record is preset to 1, and the Quantity is non-zero. If VolumeIncludesQty is true, the Quantity is 1. However, if an item is optional, the Integer Bit record is preset to 0. When the value of Bit is set to 0, the Quantity is irrelevant.

More than one application can initialize the same item. A more generic name is “Owner.” This does not limit what initializes a Pricing record to an application; anything could. That “thing,” whatever it is, “owns” that way of initializing the item’s Pricing record. Hence the name “Owner” fits well.

PricingInit

Owner

ItemID

Bit

Quantity

The next task is at the heart of whether it is possible to implement an Open/Closed solution. Within the quotation UI, inputs that can affect multiple Pricing records can be modified. On change, that input’s value can be posted to the clipboard, followed immediately by asking for an entire quote recalculation

There can be multiple Pricing records, each with multiple inputs that do not need to be the same. A check box that signifies whether hotel service is desired is specific to items with values of HotelService and HotelServiceCost. In contrast, a Discount Percentage field can be used as an input to every Pricing record where the price is computed, but not the cost.

In Stage 1, a search was made for an existing solution to derive prices in a consistent but different way and led to using the pricing model approach. The solution exists, but you need to know where to look. It is not intuitive to search for “pricing model” on the Internet.

In Stage 2, a search can be made for an existing generic way that a set of values can be recalculated based on independently configurable inputs. The term “recalculate” leads directly to the answer, that is, a spreadsheet. Can something like the way a spreadsheet works be implemented in Pega Platform?  Why not?

A value for a spreadsheet cell is computed using expressions that can include Functions. An input to a spreadsheet Function can be a literal or a reference to a different spreadsheet cell, for example, A1. A spreadsheet cell’s Function cannot reference itself since that would be a recursive infinite loop.

An abstraction of spreadsheet cell is to say that it “isA” MemoryLocation. In Pega Platform™, a property isA MemoryLocation. A set of side-by-side columns in a spreadsheet row is akin to a page in Pega Platform, also called a field group. A rectangular area in a spreadsheet is akin to a page list/field group list in Pega Platform.  A property in Pega Platform can be identified by its unique name within a named page. In a data transform, if a step page name is not supplied, the page is assumed to be Primary.

A fitting name for a data type that captures this information is Pricing Property Source.

PricingPropertySource

Owner

ItemID

Page

Property

SourceProperty

The Page column can be left empty. If the Recalculate data transform is invoked within a Pricing record-iterating loop while the Primary page is a case, the value of a SourceProperty exists within a case-level Property. A Property column value within the PricingPropertySource table must be the name of an FSG-Data-Pricing Property, either Bit, Quantity, or DiscountFactor. Every other input used by Recalculate is derived from the live data record, namely, PricingModel and VolumeIncludesQty. The previous description is what occurs within the @baseclass RecalculatePrices data transform within the Pricing Component.

3 Pass pages to the refactor transform (PricingSummary and PricingDisplay)

PricingSummary

Getting past Stage 2 is a major hurdle toward achieving an Open/Closed solution. However, there are still some finishing touches to addressed. One of those tasks is to find a way to compute total price, total cost, and profit during a recalculation effort. The most straightforward way is to define an abstract data type that contains those three properties. A fitting name for that data type is PricingSummary.  

The Visitor design pattern is when object A offers itself to a different object B to be processed by B as opposed to object A being in control of object B. In other words, object B is in control, as object A “visits” different areas of object B’s processing logic.

A PricingSummary page type can be object A. A data transform that establishes the parameters to pass to the @baseclass RecalculatePrices data transform can be considered object B.  A parameter can be of type Page Name. A case that has an embedded page of type PricingSummary can obtain the absolute path to that embedded page by calling @pxGetStepPageReference() when that function’s step page is that embedded page. The return value from that function call can be set into a parameter named SummaryPage.  That parameter can then be passed to the Work- WorkRecalculatePrices data transform, which declares SummaryPage as a page name parameter. In Pages & Classes, SummaryPage’s class is set equal to FSG-Data-PricingSummary.

Now that a PricingSummary “object” has been defined, it can be visited by the @baseclass RecalculatePrices data transform. This visit occurs after the DerivePricing data transform has been applied to the currently-iterated FSG-Data-Pricing instance. A small amount of logic is needed to detect that the item is a cost instead of a price; that is, param.ItemID ends with Cost. If so, the item is handled differently, that is, it is assumed that DiscountFactor does not apply. Finally, a cost is added to TotalCost, a price is added to TotalPrice, and Profit is the difference between TotalPrice and TotalCost.

PricingDisplay

A more complex problem to solve is setting the value of a "pricing display property," that is, a property that exists purely to display a price or cost.  The question "Why not simply display every Pricing instance in a table layout?" must be debated.

For one, a pricing display property is less limiting. A property can be positioned wherever someone wants within a Templated View by using App Studio. There is less flexibility in App Studio when it comes to modifying the appearance of a list.

Secondly, displaying a list requires that a list data page be reinitialized from its source (for example, the database), or the list must be embedded (for example, a field group list). Re-querying a set of rows queried initially from the database immediately after re-persisting those rows can be problematic. Maintaining a list within a case's clipboard memory that is potentially persisted within a case's BLOB is wasteful once the case moves past the Quotation stage. Lastly, setting and refreshing the display of a set of pricing display properties is noticeably faster.

An obvious name for an abstract data type that houses display-only properties is PricingDisplay. There can be fewer display properties than Pricing records initialized by PricingInit, or they can be one-for-one. Again, these properties are purely used for display. They serve no other purpose.

The Visitor Pattern can again be used, but it is challenging to do so in an Open/Closed Principle way.  A case can have an embedded page of type PricingDisplay.  The absolute path to that embedded page can be obtained by calling @pxGetStepPageReference() when that function's step page is that embedded page. The return value from that function call can be set into a parameter named PricingDisplayPage.  That parameter can then be passed to the Work- WorkRecalculatePrices data transform, which declares PricingDisplayPage as being a page name parameter. In Pages & Classes, PricingDisplayPage's class is set equal to ORG-APP-Data-PricingDisplay.

It is already presumed that the ID of the item that is a cost must end in Cost. Having the name for a pricing display property representing a price ending with Price sounds logical. Still, there is no reason to add that as a constraint. A best practice is to avoid unnecessary constraints. What can be done is to define a parameter named ItemIDSuffix. If an item's ID does not end in Cost, then the ItemIDSuffix is appended to that ID. The ItemIDSuffix can be any value, including an empty string.

Having detected that a PricingDisplayPage has "visited" the RecalculatePrices data transform, all it needs to do is set the value once the assumed name of the pricing display property has been decided of that property using @setPropertyValue (myStepPage,Param.DisplayProperty,PricingSavable.Price).

4 Simplify UI configuration

Forcing a UI to call the Work- WorkRecalculatePrices data transform directly on-change, or on-click of every field used as a calculation input would impose a burden on those maintaining an application. WorkRecalculatePrices has six parameters. One parameter is Boolean “Commit,” the other five parameters passed on to @baseclass RecalculatePrices.

Because every Pricing record is recomputed anyway, an input field that is ultimately used to set the value of a Bit, Quantity, or DiscountFactor “Pricing Property Source” needs only to post its value to the clipboard and tell the server to recalculate. After the recalculation, the view is refreshed to display the results of the recalculation.

When an input field’s value is posted, a Rule-Declare-Expression can be used to derive the value of the actual “Pricing Property Source.” For example, when an Integer DiscountPercentage Property value is changed, the value being between 0 and 100, a corresponding Decimal DiscountFactor Property can be set where the value is between 1.0 and 0, respectively.

The only parameter that an input field needs to communicate to the server is whether to issue a commit. It is poor practice for a data transform to assume that a commit must always be executed. The data transform might be invoked during case processing.

The FSGPricingTest application contains a sample FSG-PricingTest-Work FSGSampleRecalculatePrices data transform with a single Boolean parameter named “Commit.”  The Proxy Pattern is then employed. A proxy is something that an object calls to simplify the interface to something more complex. As shown, the FSGSampleRecalculatePrices data transform sets the values for the five parameters that are then passed on to Work- WorkRecalculatePrices (which are then passed to @baseclass RecalculatePrices). The Reference parameter is set to .pyID, the Owner parameter set equal to the application name (for example, Booking), the ItemIDSuffix parameter set to Price, and the two Page Name parameters are set as described earlier.



Available in the following mission:

If you are having problems with your training, please review the Pega Academy Support FAQs.

Did you find this content helpful?

Want to help us improve this content?

We'd prefer it if you saw us at our best.

Pega Academy has detected you are using a browser which may prevent you from experiencing the site as intended. To improve your experience, please update your browser.

Close Deprecation Notice