Implementing Rebate Voucher Functionality with BRF+ in a Classical ERP
In a recent development project, I successfully employed the Business Rules Framework BRFplus as the rules engine for voucher-based rebates in a retail scenario. In this blog, I describe my experiences and some implementation details. The bottomline is that I fully achieved my goal of implementing the rules in the business rules framework, keeping it separate from the ABAP code, which only calls the rules with the actual parameters. The rules are integrated into the classical SD pricing procedure which now functions as a façade. The processing of the rules is of highly satisfactory performance.
The Rules of the Game
This was the requirement: customers, when ordering goods in a store, could pass some rebate vouchers to the sales person, who, by scanning the vouchers, adds them as value items to the same SD customer order that contained his ordered items, resulting in a reduction of the order’s total amount.
A potpourri of rules had been invented to handle these vouchers, like the following:
- A voucher is valid for a certain assortment, for a certain label, or even for a certain article number, or a combination of these criteria.
- A voucher can result in a percentage reduction, or in an absolute reduction.
- Some vouchers require a minimum sales total for the ordered articles to which they refer.
- There are vouchers that can be applied only once. Another voucher kind counts multiple times, if the sales total for the ordered articles to which it refers is a multiple of its minimum amount.
- Yet another kind of vouchers can be applied multiple times, but not more than N times, where the number N is a property of the voucher.
- There may be restrictions on date and time: Some vouchers apply only during certain “Happy hours”, others only on certain days of the week.
When I read these rules, it was clear to me that they will soon be modified or extended by new rules. If the rules are deeply hidden in the ABAP code, it would be cumbersome to extend them. If the rules could somehow be written down in a more business-like manner, it would be easier to modify or extend them later on.
Ideally, a program implementing a certain development request should read “almost” like the rules as they are written in that request. It is an advantage if the business rules can expressed in a language which is as near as possible to natural language. Even when we decide to let only programmers change the code, it is of value to have it business-readable, as Martin Fowler points out in an interesting article on this topic:
If business people are able to look at the DSL code and understand it, then we can build a deep and rich communication channel between software development and the underlying domain. Since this is the Yawning Crevasse of Doom in software, DSLs have great value if they can help address it.
Indeed, business-readable code can be seen as a contract between programmer and business expert: A reference point for discussions on the actual system behaviour and on its possible future changes. From another viewpoint, it is a machine-readable specification. Basically, this is true for source code in general, as Jack W. Reeves pointed out in his now famous essay The Code is the Design (1992). But what turns a design document into a specification is that, in addition to being machine-readable, it is business-readable (which is not the case for the average source code of a project).
Since I am interested in this kind of questions, I thought this was a good topic to give the Business Rules Framework BRF+ a try.
We are on SAP_BASIS 702, so some features of BRF+ were missing. It came out that these gaps were insignificant and not essential for the rules at hand. There was only one OSS note to apply, concerning the internal handling of floated decimals. But even that note was not really essential, it only made the life easier.
Sales orders are created as normal SD orders, the vouchers representing a new kind of articles, resulting in value items of the sales order. Since we are in SD, prices are determined by a pricing procedure. Wouldn’t it be wiser to stick to the pricing procedure and use the “formula and conditions” exits (transaction VOFM) to implement all these rules?
Not necessarily! Bringing BRF+ into play brings the advantage that the rules determining the rebates are more transparent. If they are hidden in some ABAP form routines, the rules are difficult to find and will potentially become too complex.
Linking SD Pricing with BRF+
The idea was to use pricing formulae in the way of a façade for BRF+:
- Certain events of the sales order, e.g. if an item has been added or removed, if an ordered quantity or article number has been changed, and the like, trigger the BRF+ (re-)evaluation of the complete sales document.
- The results of the BRF+ computations are captured as attributes of an adapter class, which then effectively calls the BRF+ function.
- When the pricing procedure is propagated, certain formulae retrieve the amount for their condition line from the BRF+ adapter class attributes.
To illustrate this, consider the following formula which links the condition values with the results of BRF+ (it’s not precisely how it is implemented in our system, but it exposes the basic idea). This is the code for a custom formula 930, which is attached to the relevant conditions ZFRC and ZPRC in the SD pricing procedure:
form frm_kondi_wert_930. data: lo_rebates type ref to zcl_rebate_voucher. * Default values clear xkwert. * Instance contains the computed reductions lo_rebates = zcl_rebate_voucher=>get_instance( ). case xkomv-kschl. when 'ZFRC'. xkwert = lo_rebates->get_absolute_reductions( komp-kposn ). when 'ZPRC'. xkwert = lo_rebates->get_relative_reductions( komp-kposn ). endcase. endform. "FRM_KONDI_WERT_930
For this to work, it is essential that the single instance of zcl_rebate_voucher has processed the BRF+ rules before the pricing is called. This can be assured within the programming model of the SD sales order.
I am leaving aside for simplicity some peculiarities: For example, we are keeping track of the price bases of all sales items in an intermediate item pricing sum (VBAP-KZWI5 in our case). This is filled after the pricing of an individual item, but before the pricing of the complete document is processed (in subroutine PREISFINDUNG_GESAMT) later on. This way, the problem of reciprocal dependency (BRF+ needing SD pricing, and SD pricing needing BRF+ results) is circumvented.
To give you an impression of how fluent the rules read in the BRFplus environment, the following is a copy/paste from a central Loop Expression of the rebate ruleset. No tricks! The text is almost precisely what you read in the BRF+ detail view for that expression. In a sense, this is the source code of the new rules. If something has to be changed, it has to be done on this level.
In the BRFplus UI, the underlined terms are in fact hyperlinks, pointing to other BRFplus objects, mainly to expressions or data objects. The whole source code of the rebate rules can therefore be considered as a hypertext document, consisting of various expressions, rules and functions referring to each other. Each part of this web consists of some lines of text in natural (although formal) language.
I consider this a highly important shift in programming: On top of the host language ABAP, the business rules framework enables a new way of implementing business logic. The new meta level is fully devoted to the business domain, providing a directly readable specification of how the program works, in terms of the domain itself – whereas the host language is reserved for the technical and implementation details.
In my eyes, this is the value and the main benefit of BRFplus: The introduction of a new level of domain-specific programming, allowing a clearer separation of business concerns from “implementation details”.
It would be possible to abstain from that new level, using only ABAP to express all parts of the solution. It is then in the responsibility of the developer to write his code in different levels of abstractions, the highest abstraction on top exposing a pure domain-oriented view, the lower levels containing more “implementation-oriented” aspects of his code. This is possible by applying what Robert C. Martin calls the “step-down rule” in [Clean Code]:
We want the code to read like a top-down narrative. We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions. I call this the Step-down Rule.
Using a framework like BRFplus, one gets this separation out of the box. The rules themselves are not written in ABAP but are composites of various BRFplus expressions. Implementation details may still be written on the ABAP level (although it is basically possible to write any code in BRFplus, there is no clear benefit of it.).
Interface and Dictionary
The element which connects BRFplus into the hosting ABAP code is a BRFplus function. In our case, the function has the name “Rabatte berechnen” – “Compute rebates” and has the following interface:
All the items of the sales document are divided into two subsets: The rebate items, and the retail items, the latter being the real sales items the customer wants to buy. Before the function is called, these two subsets are filled into two internal tables, enriched by some additional properties derived from master data and sales action documents.
I found it convenient to use the tables as changing parameters, containing input and output fields in one structure. This way, when looping over the tables, the results can be filled during the rule processing. Here is an example: The line structure of the retail item table. The boxed part is the output part: The different kinds of reductions and rebate points (“Cumulus points”) the item receives, and an internal table REDUKTION_COMPOSITION, documenting the contribution of the various rebate items to the total reduction of the retail item.
In BRFplus, the word “dictionary” gets more of its original meaning, it is thought as a glossary: The pool of data objects that are needed to describe the problem from the viewpoint of the business. In contrast, the ABAP dictionary is more a globally accessible type system – an essential counterpart of ABAP source code. Nowadays, nobody considers the ABAP dictionary as a “glossary of the business terms” in the first place: Although it was once intended for this purpose, “the DDIC” now firms as part of the “technical realm” – it is as development-specific as the source code itself.
Always when we have the freedom to invent names and texs for items in programming, we should choose them wisely. In his book “Clean Code”, Robert C. Martin recommends to change the names of functions, parameters and variables again and again during development and during refactoring sessions, until the code that calls these functions is as expressive and readable as possible.
This holds for the BRFplus dictionary as well. After having invented the names and texts, we should look how the rule code reads with these texts: Is it comprehensible? An improper naming distracts from the essential point, the intention of the code / the rule.
In my project, I created the data structures in the ABAP dictionary and used the option to import them into BRFplus. It would also be possible to define all the necessary types and data objects freely in BRFplus, with no reference to the ABAP dictionary. But at least the data involved in the BRFplus function signatures should be known in ABAP as well. This means a duplicate definition of semantically identical types: a necessary “Don’t Repeat Yourself” violation, which is handled best by using the ABAP dictionary binding mechanism (the alternative: to define the type in both worlds independent of each other, would be worse). When the structure is changed later in the ABAP dictionary, it can be actualized in BRFplus.
I wrote that I use my interface parameters as changing parameters. Strictly speaking, this is not true. I found out that the parameter passing to the BRFplus function always is by value: The parameters are moved into the function context before the function call; and after the call, the results have to be pulled out from the context. This means, there are some copy processes of data necessary. The method RABATTE_BERECHNEN of the adapter class is defined with changing parameters:
methods rabatte_berechnen importing is_timestamp type zcrc_timestamp optional changing ct_retail_item type zcrc_retail_item_tab ct_promo_item type zcrc_promo_item_tab raising zcx_error .
This is the implementation of this method: the BRFplus call, which shows the necessary data copying from and into the method’s changing parameters:
method rabatte_berechnen. data: lo_rabatte_berechnen type ref to if_fdt_function, lo_context type ref to if_fdt_context. * A voucher always has a range of articles to which it refers assert id zdev condition coupons_haben_artikelbezug( ct_promo_item ) eq abap_true. * A voucher with multiplicity 1 always has a minimum turnover value specified assert id zdev condition mindestumsatz_gepflegt( ct_promo_item ) eq abap_true. * Function processing only necessary if there are retail items and rebate items check : ct_retail_item is not initial, ct_promo_item is not initial. * Get handle to BRFplus function lo_rabatte_berechnen = get_function_rabatte_berechnen( ). lo_context = lo_rabatte_berechnen->get_process_context( ). * Pass parameters into function context lo_context->set_value( iv_name = 'RABATTPOSITIONEN' ia_value = ct_promo_item ). lo_context->set_value( iv_name = 'RETAIL_ITEMS' ia_value = ct_retail_item ). lo_context->set_value( iv_name = 'ZEITSTEMPEL' ia_value = is_timestamp ). * Execute function process_function( io_function = lo_rabatte_berechnen io_context = lo_context ). * Retrieve parameters from function context lo_context->get_value( exporting iv_name = 'RABATTPOSITIONEN' importing ea_value = ct_promo_item ). lo_context->get_value( exporting iv_name = 'RETAIL_ITEMS' importing ea_value = ct_retail_item ). endmethod.
I tried to keep the ingredients of the BRFplus function self-contained, avoiding interferences with the ABAP world. Here is the set of expressions that were necessary for the rebate voucher rules:
As you see, I used the following expression types:
- Boolean expressions,
- Function Calls, and
In particular, I didn’t use:
- Database accesses from within BRFplus. If database accesses were necessary, I did them before calling BRFplus and passed the relevant data using the function parameters.
- ABAP function and method calls from within BRFplus. ABAP function calls and method calls being necessary in an essential way, would question the use of BRFplus at all. Indeed, things like custom-defined, ABAP-implemented formulae should be the exception, not the rule when using BRFplus.
There might be cases in which these operations make sense. For example, ABAP method calls may be employed if there already exists a set of complex ABAP methods, and BRFplus is used only as a tool to combine (to “orchestrate”) these methods in a flow, according to certain rules. Or, the database access may be used for techniques similar to the condition table access of classical pricing.
But keeping the BRFplus isolated and publishing all its dependencies in the function interfaces, brings a couple of advantages. One of them is the ability to do unit tests.
Unit Tests and Debugging
Designing a function such that the result depends only on the actual parameter values with which it is called, makes it easily testable, for example with unit tests. I have assured the basic rebate voucher functionality with 42 unit tests, organized in 12 classes.
In each test class, the BRFplus function is called precisely once, in the class setup. (Here, I use a macro set to fill internal tables, reducing the code to the essential parts: Which components should be filled with which values, whereas the boilerplate ABAP code to populate structures and tables is hidden in a subroutine pool zut_au_forms.)
method class_setup. data: ls_promo_item type zcrc_promo_item. _insert_row_n_fields gt_retail_item 'posnr;matnr;bossnummer;reduktion;preisbasis;waehrung;promofaehig' : '10;M1;220112548789;;1000;CHF;1'. _set_struc_n_fields ls_promo_item 'posnr;matnr;typ;betrag;mindestumsatz;mehrfach' : '90010;RABATI;5000;10;;1'. _insert_row_n_fields ls_promo_item-sortiment : "insert the article range of this voucher 'aktnr;pos_type;id;excluded' '12345;1;2201;0'. insert ls_promo_item into table gt_promo_item. clear go_testee. rabatte_berechnen( ). endmethod. "class_setup
The tests themselves only check that the function produced the expected results – like the following:
method faktor. assert_equals( act = gs_promo_item-faktor exp = `0.01` msg = `Faktor has to be 0.01` ). endmethod. "faktor method composition. field-symbols: <ls_promo_comp> type zcrc_bonus_composition. read table gs_retail_item-reduktion_composition assigning <ls_promo_comp> index 1. assert_subrc( act = sy-subrc msg = 'Voucher must be part of the item's reductions' ). assert_equals( act = <ls_promo_comp>-promo_posnr exp = '90010' msg = 'Voucher item number needs to be passed into item composition table' ). assert_equals( act = <ls_promo_comp>-betrag exp = 10 msg = 'This voucher must give a 10 CHF reduction' ). endmethod. "composition
The unit tests can be repeated frequently during development, since they don’t consume much time. If one or more of them are broken at a certain point, it must be due to the last changes. A static analysis of these last changes usually helps understand the cause why they are broken. The time to fix a new bug is usually much shorter than with a trace or debugging session.
It should be mentioned that usually there is no necessity to debug the BRFplus logic, since there is a detailed trace which logs the result and all the details of each processed expression. I’ll come back to that in a moment.
But even when you decide to debug, the module tests are helpful. By placing a break-point at the function call which produced a test failure, and hitting “Module test”, the call can be repeated easily, again and again. The BRFplus rules are translated into ABAP code (code generation). But the good news is that the generated code is readable and the generation concept is readable and comprehensible – hence debuggable.
Understanding the Result
The backoffice workers – usually not the sales persons themselves – involved in the sales order management, sometimes want to know why a particular voucher is accepted or not accepted, and how it contributes to the order’s total reductions. For these cases, an analysis screen is helpful. I designed such a screen and made it available with a button in the GUI status, using a new function code in the screen sequence control (transaction VFBS).
If the user hits the new button, the following screen is displayed, showing all the relevant data for the rebate voucher processing – including the results, which are computed again in simulation mode when the screen is called.
By double-clicking on a retail item, a popup appears listing the contributions of the different voucher items to the total reduction of this item.
For the daily business, the informations on this screen contain all the necessary information. But in support, sometimes a more detailled trace will be necessary. For this reason, I made the so-called technical trace of BRFplus available in the above screen. It can be used directly in the BRF+ Web Dynpro application, as described in a blog by Phillip Parkinson but then the interface data would have to be entered manually, which is a time-consuming and error-prone business for our function: Two internal tables would have to be populated field by field, row by row – with data that are available in the order anyway. In order to populate the interface automatically with the current sales order data, I decided to call the trace in ABAP code. This is the method performing the trace call:
method process_function. go_context = io_context. call method io_function->process exporting io_context = go_context iv_trace_mode = if_fdt_constants=>gc_trace_mode_technical importing eo_trace = go_trace eo_result = go_result. endmethod.
The trace object eo_trace passed back from the function call, contains an XML representation of all the evaluations performed during the call. This trace is very detailed. The support employee, when analyzing a particular problem, can call it using the “XML” button in the detail screen shown above:
Due to its very detailed nature, I felt the desire to transform this XML into a more readable subset. There are certain building block elements of the XML structure like FUNCTION, EXPRESSION, RULESET, CONTEXT and CONTEXT_UPDATE which are combined in a hierarchical manner (reflecting the rules implementation, of course). I therefore designed an XSLT transformation ZCRC_TRACE which, applied to this XML file, produces HTML code in a more reduced manner, as the following screenshot shows. There are buttons for collapsing or expanding the subordinate informations on each level. Data objects can be inspected by clicking on the corresponding hyperlink, which shows them using the function module RS_COMPLEX_OBJECT_EDIT, which is also used for data maintenance and inspection in function module single test mode (SE37).
My experiences with this first BRFplus project are very positive. The implementation is stable and efficient. The vast majority of bugs and feature requests in the test phase referred not to BRFplus itself, but to the embedding of the rule processing into the SD sales order.
I consider BRFplus as a serious step towards the ideal of business-readable code. The introduction of a new level of programming, on top of ABAP as the host language, is a promising new route in development.
Also, I praise the co-operative philosophy of BRFplus: It doesn’t want to lead, to seize the whole development floor, but can co-exist with other implementations which at the same time are following a more traditional approach. There is no top-down strategy necessary (by the way, it was this top-down philosophy which broke SOA the neck). In the words of blogger Meinte Boersma: BRFplus, like all the model-driven-inspired development techniques, needs a kind of guerilla technique. Don’t make a big management issue about it – just employ it in all places where you find it makes sense. Every aspect of the system that gets implemented with BRFplus is your contribution to the better software world of tomorrow! 🙂
- Thomas Albrecht and Carsten Ziegler: BRFplus – Business Rule Management for ABAP applications. SAPpress 2010.
- Phillip Parkinson: Technical Trace in BRFplus. Blog, 09/19/2011.
- Meinte Boersma: In the trenches: MDSD as guerilla warfare. Blog, 10/01/2012.
- Martin Fowler: Domain-specific languages. Addison-Wesley 2010.
- Robert C. Martin: Clean Code. A handbook of Agile Software Craftmanship. Prentice Hall, 2008.
- Jack W. Reeves: Code as design. Three essays, The C++ Journal, 1992f.