Lab Info
Lesson | Agreements & Promotions |
Category / Topic / Section | Pricefx Core / Agreements & Promotions / CFG 2 |
Target Audience | Certified Configuration Engineer |
Estimated Time to complete | 1:00 |
Learning Outcomes
At the end of the Lab, you should be able to:
implement 2nd Condition Type with different conditions than in Lab 1a
implement simple impact analysis, to review impact of the negotiated conditions on the business
Figure 1. Sneakpeek of the implemented Promotion Condition Type
Pre-requisites
In order to be able to complete this laboratory exercise, please complete the prerequisites before you proceed.
Provided resources
Review the provided resources, to get familiar with their content
Datamart "Transaction"
Labs
This lab expects, that you already have in your partition a solution of the preceding labs:
Agreements & Promotions Engineering - Lab 1a
If you need to work on this lab without finishing the preceding ones first, you can deploy the solutions of the preceding labs provided in a ZIP file attached to the course.
User Story / Requirements (for Lab 1a & 1b)
As a Sales Manager, I want to negotiate 2 types of promotion conditions with the customer, so that I can give incentive to the customer and increase sales.
As a Sales Manager, I want to see an impact analysis on each line of contract, so that I can easily compare the benefits of the contract with losses.
Details
The 2 types of promotion conditions are:
Promotion Discount
the size of the on-invoice discount is a fixed percentage value
the discount is negotiated for set of products, selected by the manager
Volume Discount
the size of the on-invoice discount depends on the quantity/volume negotiated. So the user need to enter a several pairs of values, for various tiers/levels - e.g.:
when more than 5 pieces are ordered, the discount should be 1%
when more than 10 pieces are ordered, the discount should be 2%
the discount is negotiated for:
set of customers
set of products
The 2 types of on-invoice discounts have a separate adjustments in a waterfall calculation, so they can be later (e.g. on Quote) both used at the same time.
Acceptance Criteria (for Lab 1b)
Main:
A Condition Types exist for "Volume Discount"
for "Volume Discount"
User can enter:
Customer Group
Product Group
Promotion Discount size in %
Upon Recalculation, the system provides:
Revenue Last Year - how much money the selected group of customers paid us on invoice price last year. Last year means, that you take the period specified by the user, and shift it back by 1 year. Invoice price data are available in Datamart "Transaction".
Impact on Revenue - how much money would we pay to the group of customers last year (if this discount would be active already last year)
Other:
the product and customer groups are negotiated separately on each promotion line type, i.e. no need for the product and customer group selector on the contract header.
Implementation Steps
If you already have some previous knowledge about implementation/configuration of the PromotionManager module, we encourage you to try to implement it first on your own based on the information provided in the User Story and Acceptance Criteria.
Step: Implement Condition Type for "Volume Discount"
In Studio, create a new Calculation Logic of Nature Contract.
Name it "ContractVolumeDiscount"
for label use "Volume Discount".
The Validity will be moved backwards, in case you would like to experiment with data from the previous year.
The user wants to select a set of customers, add Element "CustomerGroup" with code:
final String INPUT_NAME = "CustomerGroup" if (api.isInputGenerationExecution()) { api.inputBuilderFactory().createCustomerGroupEntry().getInput() } else { return CustomerGroup.fromMap(input[INPUT_NAME]) }
this Element should not display its value in results, unless you want to use that for debugging
As the user wants to select a set of products, add Element "ProductGroup" with code:
final String INPUT_NAME = "ProductGroup" if (api.isSyntaxCheck()) { api.inputBuilderFactory().createProductGroupEntry().getInput() } else { return ProductGroup.fromMap(input[INPUT_NAME]) }
this Element should not display its value in results, unless you want to use that for debugging
the user need to enter a several pairs of values, for various tiers/levels of the Promotion Discount thresholds. These values also need to be on the Price Record later. Add a new Element "VolumeDiscountMap":
final String INPUT_NAME = "VolumeDiscount" final String INPUT_LABEL = "Volume Discount" final String COLUMN_QUANTITY = "Quantity" final String COLUMN_DISCOUNT = "Discount %" if (api.isInputGenerationExecution()) { api.inputBuilderFactory().createInputMatrix(INPUT_NAME) .setLabel(INPUT_LABEL) .setColumns([COLUMN_QUANTITY, COLUMN_DISCOUNT]) .setColumnValueOptions() .getInput() } else { /* [ {Quantity=5, selected=false, Discount %=1}, {Quantity=10, selected=false, Discount %=2}, {Quantity=50, selected=false, Discount %=3} ] */ return input[INPUT_NAME] ?.findAll { (it[COLUMN_QUANTITY]) && (it[COLUMN_DISCOUNT]) } ?.collectEntries { [ (it[COLUMN_QUANTITY] as BigDecimal) : ((it[COLUMN_DISCOUNT]) ? ((it[COLUMN_DISCOUNT] as BigDecimal) * 0.01) : null) ] } }
the element should not be displayed in the results, unless you want to see the value for debugging purposes. In that case, do not set any formatting.
For calculations, we need to retrieve the information about Start Date, which the user is entering on the contract header. In the Contract Header logic, you’re passing this value to the binding variable
api.global.startDate
. The global variable keeps the values across the whole re-calculation of the contract, so we can read it on the line item. Add a new Element StartDate:if (api.isDebugMode()) { api.global.startDate = api.inputBuilderFactory() .createDateUserEntry("startDate") .getInput() } return api.global.startDate
the input will be used only, when you Test Logic in Studio, becasue there you will not have the information from the Header available.
In the same way, we need also the endDate. Add new Element EndDate:
if (api.isDebugMode()) { api.global.endDate = api.inputBuilderFactory() .createDateUserEntry("endDate") .getInput() } return api.global.endDate
As you do not need any more input fields, let’s abort the logic, if it is executed in Syntax Check mode. Add a new Element AbortOnInputGeneration with code:
if (api.isInputGenerationExecution()) { api.abortCalculation() }
after the contract is approved, you would like to have in the Price Record also the name of the Condition Type. So you will find out the Condition Type of the current line and store it on the line item, so that it is later copied to the Price Record. Make new Element "ContractTermType" with code:
return api.currentItem()?.contractTermType
ensure this value is available in results
To be able to pass the Volume Discount tiers entered by the user to the PriceRecord you will need to change the shape of the result little bit, so that it maps to Price Record well and we are able later to read and use it from there. Add new Element "VolumeDiscount" with code:
return api.jsonEncode(out.VolumeDiscountMap)
The user would like to see the impact of their decisions on the business. Since we cannot see the future, the customer asked to use data from last year as an estimation for the following months. Add a new Element "RevenueLastYear"
the result of this element should be visible in the results
set the Format Type to Money (EUR)
with code:
final String COLUMN_REVENUE = "InvoicePrice" final String COLUMN_DATE = "InvoiceDate" // Find the time period in previous year for the analysis def startDate = api.parseDate("yyyy-MM-dd", out.StartDate) startDate?.set(year: (startDate ? (startDate[Calendar.YEAR] - 1) : null)) def endDate = api.parseDate("yyyy-MM-dd", out.EndDate) endDate?.set(year: (startDate ? (endDate[Calendar.YEAR] - 1) : null)) def ctx = api.getDatamartContext() def q = ctx.newQuery(ctx.getDatamart("Transaction")) .select("SUM(${COLUMN_REVENUE})", COLUMN_REVENUE) .where( Filter.greaterOrEqual(COLUMN_DATE, startDate), Filter.lessOrEqual(COLUMN_DATE, endDate) ) if (out.ProductGroup) { q.where(out.ProductGroup) } if (out.CustomerGroup) { q.where(out.CustomerGroup) } return ctx.executeQuery(q)?.data?.find()?.getAt(COLUMN_REVENUE)
CAUTION: the value of
out.ProductGroup
was selected on top of Product Master table, but the filter is used for the Datamart fields! The mapping is going from ProductMaster to DS Products and it’s fields are brought to the Datamart. You MUST ensure, that all the columns the user can select in the filter, will also be available in the Datamart, otherwise the Query will fail.the same rule as in #1Datamart queries at the line item level are potential performance risk, especially, when there are a lot of line items in the Contract. In such case it would be good to consider pre-caching already on the header level, if possible.
And finally, the user would like to see the impact of the negotiated Volume Discount on the estimated Revenue (found in the previous element). In this case, it’s quite simplified for purpose of training. Add a new element "ImpactOnRevenue" with code:
if (out.RevenueLastYear != null && out.VolumeDiscountMap) { def worstPromotionDiscount = out.VolumeDiscountMap ?.sort { -it.key } ?.find()?.value return out.RevenueLastYear * worstPromotionDiscount * -1.0 } else { api.addWarning("Cannot calculate the estimated impact on Revenue, " + "because either Last Year Revenue or Promotion Discount" + "is unavailable.") }
this value must be in the results
set the Format Type to Money (EUR)
Deploy the Logic ContractVolumeDiscount to the partition
It would be the best to be able to test the logic right now, but before we can test the Logic, we need to have the Condition Type prepared, otherwise you cannot test the Contract Logic in Studio.
Create new Condition Type
in Pricefx Unity, navigate to Agreements & Promotions Condition Types
use Add Condition Type and set up the new type
set the Name to "VolumeDiscount"
set the Label to "Volume Discount"
set the Pricing Logic to your newly deployed ContractVolumeDiscount logic
the Waterfall Element is optional, and is used to indicate, which adjustment element of the waterfall is influenced by this Condition Type. This value is usually not used in the Contract Logic, but later (e.g. on a Quote), when the Contract conditions are applied.
click on Add to add the new type
Test the logic
On logic’s Parameters tab, remember to click Generate Parameters to see all inputs
Setup all the inputs and Test Logic
remember to select the ContractTermTypeName, otherwise you will get unexpected error while trying to Test the Logic.
TargetDate would have effect only if you’re reading data from PriceParameters, or if you would explicitely use the TargetDate in some calculation. In this case you can simply keep today date.
for the Volume Discount tiers/levels, ensure to add at least 2-3 tiers, so that you can see how the map looks like
Test the logic and verify the results. Ensure it works all ok before moving to next steps.
Test the Condition Type in the Unity interface
in Pricefx Unity, navigate to Agreements & Promotions Agreements & Promotions
Click on + New Agreement & Promotion
set the Start Date and End Date to be a range of the whole year (regarding your today)
the Calculation Date will influence, which Price Parameters versions are used data are used for calculations, so it would likely be either day of calculation (usually "today"), or the first day of validity of the Contract.
move to Items tab
add the line with VolumeDiscount Condition Type
Select your new Condition Type VolumeDiscount as new line
Select the Customer(s) filter
Select the Product(s) filter
enter the Volume Discount tiers
Click on Recalculate to recalculate the contract and all of its line items
Review the Calculation Results
Save your contract, you will need it later
Step: Setup Mapping of the Contract to Price Record
For either export of contract conditions to external systems, or for easier retrieval of negotiated promotion conditions from another modules, it is useful to export the Condition Type lines into Price Records.
The out-of-the-box fields are mapped automatically, but for your new fields, you have to setup the mapping. The mapping is done only by matching of names - the name of the result(or input) on the line item must match the name of the column on Price Record.
Both input fields and output fields can be mapped to the Price Record.
In Unity, navigate to Promotion Manager Price Records
Review the column ConditionType, which keeps the name of the Condition Type, so that we know, what type of conditions are stored on that line. This column was created in Lab 1a. The value of the column will come from the result ConditionType of the line item.
Setup a column to store the Volume Discount percentage value. The value of the column will come from the input field VolumeDiscount of the line item.
Rename and Customize the column attribute3
set Name to "VolumeDiscount"
set Label to "Volume Discount"
set Type to String
Verify the mapping of the Contract line to the Price Record
in Unity, navigate to Agreements & Promotions Agreements & Promotions
open the previously saved Volume Contract (click on its ID)
Submit the contract for approval
since there’s no approval workflow now, it does not require any approval, and so gets through immediately, and the mapping of the contract line items to the Price Records happens also immediately.
in Unity, navigate to PromotionManager Price Records
Verify the new record in this list
you should find there a new record, with SourceID the same, as your Contract ID.
Customer Group should match your selection on the contract line item
Product Group should match your selection on the contract line item
Valid After and Expiry Date must match to your selection of Start Date and End Date on the Contract Header
Condition Type is mapped from the results of your contract line item
Volume Discount is mapped from the input of your contract line item
Complex input / output types like this are represented as JSON in the Price Records and so you need to accommodated it in the output if this is sent to external systems.
As an example, some external systems will need a row per volume break - meaning Platform or some other method (perhaps moving to a DS) will need to "explode" the one price record in to many rows.
Example of a Solution
References
Developer Documentation
Documentation (Classic)
Older Training Materials
Accelerators (non-public)