HRM

  • Components

    Components form the basis of our HRM system. Each component represents compensation and benefits an employee can receive as part of their salary package. This document describes what components are and how they are used throughout the entire system. As you will see components relate closely to policies, the only difference being that components are based on law, sectoral and institutional rules. They are defined by law and therefor informatively enforce those laws.

    • Policies

      Policies is a concept that drives the application. It is a new paradigm focussed on achieving truly dynamic applications. It is different from usual concepts for applications. Although the reference architecture is one that is widely known and is worked on by each and every organization. The basis for the policy system can be found in the security realm. Consider 2 types of applications: Data driven: Data is highest good, we have input and need to convert it to output Process driven: A process defines how an object moves through its different states, the data in that object becomes secondary Here we define a new way to approach a dynamic system.

      • Client provisioning

        Client provisioning is the process of seting up and deploying a container for a new client. In this project we will focus on self-service setup. The idea is that a client can set up new environments and has access to them via single sign on. Process CamundaClient provisioning The process needs to differentiate between a new environment and an existing one. The steps to create or update an environment differ.

        • Dynamic routes

          Dynamic routes are used to load layouts based on a structure in url format. These simplify the work needed to setup a page and allow for easy communication between dynamic components. Requirements Fixed structure Clean URL compliance wiki Unlimited length Component parameter support Pass information up/down the component tree Support commands Fixed structure Consider this url as an example: https://my-tool.org/employees/details/id/aaa-aaa-aaa/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc The url has the following parts: host https://my-tool.org: this denotes the application url.

          Subsections of HRM

          Components

          Components form the basis of our HRM system. Each component represents compensation and benefits an employee can receive as part of their salary package. This document describes what components are and how they are used throughout the entire system. As you will see components relate closely to policies, the only difference being that components are based on law, sectoral and institutional rules. They are defined by law and therefor informatively enforce those laws. Within the system they are immutable, although they can be influenced by policy. Law provides us with constraints where organizations are free to define their own policies within these constraints. The first order of business is providing a system that can capture these components and their constraints. This is the job of components.

          Note

          The difference between components and policies is:

          • Components have a legal character, follow institutional law and are therefor immutable. They are controlled on the highest level.
          • Policies is what clients and users can manage to tailor the application to their needs, including the input and results components use and provide.

          Why do we need components

          Components in our system have a couple of goals. Most essential is that components form the input for salary composition and form the input for payroll engines. The scope of our software is to provide a tool allowing for flexible compensation and benefits for employees. The goal is to make this manageable for the employer by providing in reporting and cost estimations. Additionally, the intent is to create insight for employees into their salary composition by providing a clear overview of all components that make up their salary.

          graph TD;
              A[Labour law] --> X(Component)
              B[Sectoral agreements] --> X
              X --> C[Payroll]
              X --> D[Reporting]
              X --> E[Information]
          
              C ==> F(Benefit in kind)
              C ==> G(Budget impact)
              D ==> H(Total cost of ownership)
              D ==> I(Cost optimization)
              E ==> J(Simulation)
              E ==> K(Visualization)
              E ==> L(Comprehensible)
          
              classDef green fill:#9f6,stroke:#333,stroke-width:2px,color:black;
              classDef orange fill:#f96,stroke:#333,stroke-width:4px;
              class X green
              class di orange
          

          Example component

          To clarify components, we will take the example of an electric bike. Let’s crunch the numbers. In this example an employee has chosen for an electric bike with a consumer price equal to 2799.00.

          Result Formula
          residualValue 2351.16 consumerPrice - (consumerPrice * residualPercentage)
          RSZemployee 307.30 residualValue * RSZemployeePercentage
          companyTax 817.55 ((residualValue - RSZemployee) * companyTaxPercentage
          cityTax 0.00 ((residualValue - RSZemployee) * companyTaxPercentage * cityTaxPercentage
          Netto 1226,32 residualValue - RSZemployee - companyTax - cityTax

          Additionally we want to calculate the budget value.

          Result Formula
          tax 0 consumerPrice * taxRate
          residualValue 447.84 (consumerPrice - tax) * residualPercentage
          RSZemployerVAA 0 VAAbicyle * componentPeriod * companyContribution
          companyTax 817.55 ((residualValue - RSZemployee) * companyTaxPercentage
          budgetValue 2351.16 (consumerPrice - tax - residualValue + RSZemployerVAA) / installments

          To be able to calculate the values we need, we need some information. These system needs to provide these parameters somehow, or we can’t complete our calculations.

          Parameter Value
          consumerPrice 2799.00
          residualPercentage 0.16
          RSZemployeePercentage 0.1307
          companyTaxPercentage 0.40
          cityTaxPercentage 0
          taxRate 0
          VAAbicycle 0
          componentPeriod 36
          companyContribution 0.28
          installments 1
          Note

          A lot of information can be found here. Without specifying where the values come from, we can see that we need about 10 parameters to be able to calculate a single component. Notice that we don’t specify where these parameters are coming from. This is because these parameters differ based on:

          • sector
          • product classification
          • tax rates
          • contract
          • residential address
          • employer contributions

          This requirement is where policies come in. We need a way to resolve these values given a set of conditions. Additionally, we need to be able to get these parameters from different corners of the application, we need to be able to set default values, we need to be able to overwrite them to allow for cost optimization and much more. The only mechanism than can cover these use cases is policies. Check out the policies page for more information.

          Defining formulas and parameters is all well and good. Although without a way to trigger calculations, or to resolve values, the ability to do calculation has no value. We need to solve a few more problems. Firstly we need to define the input and output of a component.

          Component IO

          To begin to describe how calculations behave within the system, we need a use case. The first use case we will be considering is that of flexible salary. Flexible salary allows employees to assign a part of their income to their own discretion. Al be it, as always, within the boundaries of the law. To achieve this, we need to link components to employees and/or their contracts. As we determined, components make up an employees salary. Therefor it is the declaration of these components in relation with these employees that starts it all. The question becomes, what are we linking, and how do we identify links? To answer that question, we will be linking multiple concepts through the same system, e.g.:

          • Legal requirements: Monthly/hourly salary
          • Flexible products chosen/ordered by the employee
          • Compensation and benefits as negotioated with the employer

          The thing to notice is that the link can take many forms. Therefor basing it on one of these forms would not be practical. Eventually we would need to implement logic that rhymes these corners of the application. The result would be messy, trying to align different use cases together, in a logical, sequential and potentially impact with legal ramifications, could result in: high cost for analysis, slowing progress and complex unmaintainable code.

          To mitigate these issues, we will define a standard input/output format for the formulas. One that allows for linking to any and all other concepts both internal and external to the own organization. This would allow for a highly standardized approach that will help to focus on the value components provides. The responsibility of connecting to the component engine will be relayed to the using side. It would be best placed there since it adheres to encapsulation principles.

          So we need to define our own object to use as input and output for the component system.

          Input

          The input for calculations is the actual link. It will contain all values available from the origin. And it will add the configuration for the engine to process the calculations.

          Note

          The one constant to all links, is the component. It ties it all together.

          Important

          To keep documentation consistent and readable for non-technical people, I will be defining the objects as yaml. However the system will use json as a format under the hood.

          Let’s define an object that can contain the links we need for the calculation engine. Additionally, input is required for calculations to be possible. Therefor when creating the entry, we will add the values as they are available.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          
          kind: component-values
          component: mobilePhone          # component technical name
          version: latest                 # indicates to always use the latest version for calculation
          date: 20240101100000            # date the entry was created
          origin: flexplan-order          # reference to the origin
          origin-id: 1234                 # id for the reference
          links:
            - employee: aaa-aaa-aaa       # employee linked to the order
            - flexOrder: xxx-xxx-xxx      # order that created this link
            # more links are possible
          values:
            - consumerPrice: 2799.00      # product price as chosen by employee in the order
            - residualPercentage: 0.16    # residual percentage as part of a lease/order
            - taxRate: 0.0                # tax rate provided by the product categorization

          At this point an entry is made but no calculations have been performed. We need to define how calculations are handled. It is up to the component to tell the system how the calculations should behave. This depends on the context of the component. Let’s add some example calculation definitions to the entry:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          
          kind: component-values
          component: mobilePhone          # component technical name
          version: latest                 # indicates to always use the latest version for calculation
          date: 20240101100000            # date the entry was created
          origin: flexplan-order          # reference to the origin
          origin-id: 1234                 # id for the reference
          links:
            - employee: aaa-aaa-aaa       # employee linked to the order
            - flexOrder: xxx-xxx-xxx      # order that created this link
            # more links are possible
          input:
            - consumerPrice: 2799.00      # product price as chosen by employee in the order
            - residualPercentage: 0.16    # residual percentage as part of a lease/order
            - taxRate: 0.0                # tax rate provided by the product categorization
          config:
            start: 20240101               # date calculations should start
            end: 20250101                 # date calculations should end
            interval: * * 1 * *           # cron expression indicating recurring calculations

          Policies

          Policies is a concept that drives the application. It is a new paradigm focussed on achieving truly dynamic applications. It is different from usual concepts for applications. Although the reference architecture is one that is widely known and is worked on by each and every organization. The basis for the policy system can be found in the security realm.

          Consider 2 types of applications:

          • Data driven: Data is highest good, we have input and need to convert it to output
          • Process driven: A process defines how an object moves through its different states, the data in that object becomes secondary

          Here we define a new way to approach a dynamic system. Policies are the highest goods and they influence the data/fields shown in the UI. They define the process to follow, security to be applied, where data can be resolved, and much more.

          How can we use all fields across our application and have them influence all other aspects of the system? The answer is policies.

          Note

          Reference architecture can be found in any/all security systems. We will use firewall rules and policies as an example to model this system.

          Policy Definition Language (PDL)

          Following the example within security, policies are defined in a single file. They have a specific syntax that is flexible and extensible. The base of the system is this ruleset of policies. It will drive the application.

          General structure could be:

          FOR [TARGET] WHEN [SELECTOR]
              RESULT [TARGET SPECIFIC KEYS]
              ON [DATETIME] TO [DATETIME]
              ROLE [ROLE NAMES] ;

          Our system is mostly Json based, so json would be a better fit. It would be easier to integrate with the existing software and methodologies. The downside is that it is not really readable for non-technical people. The idea is that knowledgeable people can quickly perform changes to this file. Therefor Yaml would be a better fit. It allows for easy conversion to and from Json and has the added benefit that it is more readable for non-technical people. So our implementation will be Yaml based.

          Important

          The policies will have a json structure trough the entire system, except for the policy file editor. There a conversion will be made to Yaml.

          The structure from above in Yaml would have this structure:

          1
          2
          3
          4
          5
          6
          7
          
          policies:
            - for: <target>
              when: <condition>
              on: <datetime>
              to: <datetime>
              role: <role_list>
              result: <result resolver>

          Parts explained:

          • TARGET: Denotes the effect of a policy (e.g: filtering lists, defining parameters, security constraints, …)
          • TARGET SPECIFIC KEYS: Each target has its own specific keys/implementation. This is where they are defined.
          • ON and TO (optional): Allows for versioning policies by date and time
          • ROLE NAMES (optional): Specifies specific roles for whom the rule applies

          Comments

          You can add comments to the file with the # character.

          1
          2
          3
          4
          5
          6
          7
          
          # This is a comment
          policies:
            - for: parameter # This is another comment
              selector: "mobilePhone.benefitInKind"
              result:
                type: static
                value: 3

          Targets

          Parameter

          Parameters are used inside (payroll) components. Components denote different aspects that make up the compensation and benefits an employee receives in exchange for their labour/services. Business requirement states that these parameters can come from anywhere. PDL is dynamic and allows for resolving these parameters from different sources. The job of the PARAMETER query is to describe how to resolve these parameters.

          Important

          For resolving paranmeters, we need a way to define a value for all components. Therefor the parameter selector will support wildcards. E.g.: when: *.companyVat. This specifies that all parameters named companyVat will have the same value. Notice that we can still overwrite the value if the need should arise.

          Static resolver

          The static resolver is perhaps the simplest. It defines the value as part of the policy.

          e.g.:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              result:
                type: static
                value: 3
            - for: parameter
              selector: "mobileSubscription.benefitInKind"
              result:
                type: static
                value: 3

          The above policies define a fixed value. This value will be applicable to all employees since the WHEN specification was omitted. The effect is that in every calculation of the components mobilePhone and mobileSubscription will have a benefitInKind with the value of 3.

          Considder the following adaptations:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              when: "employee.function.type" = "management"
              result:
                type: static
                value: 4
              
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              result:
                type: static
                value: 3

          To note here is that both rules affect the same parameter. Rules are processed in order as found in the ruleset. The effect off the above configuration is:

          1. if the function of the employee is of type management then the result of the benefitInKind for a mobile phone will be 4
          2. for all other employees the result would be 3
          Important

          Rules need to be ordered correctly, from most relevent to least relevant.

          Considder the following adaptation:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              when: "employee.function.type" = "management"
              on: 20200101000000
              to: 20210101000000
              result:
                type: static
                value: 2
            
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              when: "employee.function.type" = "management"
              result:
                type: static
                value: 4
              
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              result:
                type: static
                value: 3

          To note here is the addition of the on and to parameters that limit the policy in time. The effect of these rules are:

          1. Before januari 1st 2020 all employees in management would have a benefit in kind for a mobile phone equal to 4. The first rule does not evaluate to true because of the date specification.
          2. For the entire year 2020 all employees in management would have a benefit in kind for a mobile phone equal to 2. The rule applies since the date falls betwween the dates specified in the ON and the TO date constraints.
          3. All other employees, at any given time will have a benefit in kind of 3
          Important

          To help with the dates, the dates should be handled as:

          • on date is inclusive
          • to date is exclusive

          This allows for easy definition of start dates, and eliminates the need to do arithmatic on the end date to subtract one tick.

          Entity resolver

          The entity resolver will implement a method that allows values to be obtained from other parts of the system. It effectively allows linking a parameter to a field anywhere in the application.

          1
          2
          3
          4
          5
          6
          7
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              result:
                type: entity
                selector: "com.platfohrm.employer.BikMobilePhone"
                where: "com.platfohrm.employer.Id" = "${{env.EmployerId}}"

          This policy has the effect of resolving the benefitInKind parameter for the mobilePhone component from the entity com.platfohrm.employer.BikMobilePhone. To achieve this the service responsible for the employer data needs information to retrieve the correct record. Here we specified that the records Id should equal the EmployerId of the environment where this ruleset is active.

          Important

          The environment variables are specified on the top of the file. You can define variables and use them throughout the ruleset. This allows for copying over rules between environments. You would only need to override the variables for each environment. You can use the following format:

          1
          2
          3
          
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd

          Hierarchy

          An important reason for this implementation is the concept of hierarchies. To explain hierarchies we first need to look to the structure of clients of the application.

          The structure is as follows:

          ---
          title: Hierarchy
          ---
          graph TD;
              A[Platfohrm] -->|has| B(Expert A)
              A -->|has| C(Expert B)
              B -->|supports| E[Employer 1]
              B -->|supports| G[Empoloyer 2]
              C -->|supports| H[Employer 4]
              C -->|supports| I[Employer 5]
              G -->|devided in| J(Establishment A)
              G -->|devided in| K(Establishment B)
              K -->|structures| L(Department 1)
              K -->|structures| M(Department 2)
              L -->|employs| O(Employee A)
              L -->|employs| P(Employee B)
              P -->|contract| Q[Contract 1]
              P -->|contract| R[Contract 2]
          

          Requirement: Policies from the top level are applied, unless they are overwritten on a lower level.

          Important

          To achieve hierarchy we need a file that structures the policies in sequence. This ties together with rules that are structured from most relevant to least relevant.

          Now lets structure the policies as they are defined following this hierarchy.

          Remember: The first rule to match will be applied.

          We will start to build this hierarchical structure from the ground up. We will start with an example to determine the cost for mealVouchers. In our example this would represent a component that is part of the compensation for an employee.

          Level 0: Platfohrm

          So lets start from the top down. At the top level we define that the value of one meal voucher is 5.00. The business reason for specifying this can be all sorts. In this case we would assume its the legal minimum. So the policy for Platfohrm would become:

          1
          2
          3
          4
          5
          6
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              result:
                type: static
                value: 5.00

          Let’s say law changes starting the 1st of 2025. The value would become 6.00 due to regulatory changes. To achieve this, we would change the policy to:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              to: 20250101
              result:
                type: static
                value: 5.00
                
            - for: parameter
              selector: "mealVoucher.value"
              on: 20250101
              result:
                type: static
                value: 6.00
          Important

          Note the TO and ON parts of the policies above. When TO is specified without a corresponding ON. It means that all dates before the TO date will evaluate this condition as true. In contrast to this, a ON without a TO denotes that there is no end date specified for the given policy. All dates starting from the given date would evaluate to true.

          The effect of this policy is that prior to 20250101 the value would always be 5.00. Starting from 20250101 the value would always be 6.00.

          If you want to specify more constraints like the function classification of an employee, you can look at the previous sections of this documentation.

          To keep this example simple we will continue to the next level.

          Level 1: Expert

          The next level in the hierarchy is that of the expert. This organisation can again specify their own policies. For example: the expert wants to create a more attractive payroll packet for staff in an accounting function. One of the measures they take, is to change the value of the meal vouchers. This would result in this policy:

          FOR PARAMETER "mealVoucher.value" WHEN "employee.function.type" = "accounting" RESULT STATIC 10.00
          1
          2
          3
          4
          5
          6
          7
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 10.00

          This policy states that when employees occupy a function of type accounting then meal vouchers would get a value of 10.00.

          Level 2: Employer

          The next level in the hierarchy is that of the employer/client. He is of the opinion that 10.00 is to high. His organization is an accountancy firm and that would cost to much. Therefor the employer wants to change this value to 8.00. To achieve this he creates a policy:

          1
          2
          3
          4
          5
          6
          7
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00

          Note: The policy created here is the exact same as the Expert, but with a different value.

          Level 3: Establishment

          The organization has many establishments, but one of their establishment has an in house kitchen. They serve hot meals every day at discounted prices. Management feels that this is already a big plus for the employee at this location and wants to change the value of the meal vouchers accordingly. The employer would add a new policy to achieve this:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          Important

          Note that the new policy was added above the previous policy. It is more specific and therefor must be above the other statements. This because the first to match will be applied.

          Level 4: Department

          The organization has sales representatives on the road and would like to support the expensive food prices by increasing the value of the meal vouchers. The organization would update the policy:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.department.name" = "sales"
              result:
                type: static
                value: 9.00

          Level 5: Individual employee

          The employer got the chance to attract a senior sales representative. This person has a proven track record and getting him to join the company had proven tough. Negotiations were tough. One of the extra benefits this person asked was for a higher value of meal vouchers. He/she negotiated a value of 10. The company updated their policy to reflect this:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.Id" = "1234"
              result:
                type: static
                value: 10.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.department.name" = "sales"
              result:
                type: static
                value: 9.00
          Important

          The individual rule was placed on top to make sure it was applied before other rules were processed.

          Level 6: Individual contract

          Since the acquisition of the new sales representative sales were going great. So great the company could hardly keep up. This meant some of the sales representatives have been asked to work part-time. Their contract was amended to a part-time function. One of these employees had experience in ICT and agreed to take on additional work as a support agent within the ICT department. HR drew up an additional contract for the employee, again for a part-time, but this time in the ICT department as a support agent. An agreement was made that this person would get a value 7.50 for his meal vouchers while working the function of support agent. The company added a policy to the new contract:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          
          policies:
            - for: parameter
              selector: "mealVoucher.value"
              when: "contract.Id" = "abcd"
              result:
                type: static
                value: 7.50
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.Id" = "1234"
              result:
                type: static
                value: 10.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
                
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.department.name" = "sales"
              result:
                type: static
                value: 9.00

          This completes the example of hierarchies. All that is left is putting it all together.

          Putting it all together

          Policy file structure

          For our example of meal vouchers. All levels have created their policies. All that is left is combining these policies so that the correct policy would apply for each level of the hierarchy. The answer is simple, we load all policy files in reverse order, from the bottom of the hierarchy to the top. This would result in the following policy:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
          49
          50
          51
          52
          53
          54
          55
          
          policies:
            # Contract level
            - for: parameter
              selector: "mealVoucher.value"
              when: "contract.Id" = "abcd"
              result:
                type: static
                value: 7.50
          
            # Employee level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.Id" = "1234"
              result:
                type: static
                value: 10.00
                
            # Department level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.department.name" = "sales"
              result:
                type: static
                value: 9.00
          
            # Establishment level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
                
            # Employer level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          
            # Expert level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 10.00
                
            # Platfohrm level
            - for: parameter
              selector: "mealVoucher.value"
              result:
                type: static
                value: 5.00

          This structure will ensure that the policies are applied for the correct conditions.

          Note

          The top level platfohrm can define default values for almost any parameter in the system. As long as there is no policy that overrides this, the top level would always be applied.

          Adapting over time

          One last part of the system has to do with changes in policy on any of the levels. To achieve this we can use a similar approach as we did for the environment variables. We would start the policy file by specifying the variables on top and add a similar structure for the hierarchy:

          1
          2
          3
          4
          5
          6
          
          hierarchy:
            - base: "platfohrm:latest"
            - expert: "expertA:latest"
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd

          The above file specifies that additional policy sets are loaded from other levels. This employer works with the base as defined by Platfohrm. And they have chosen for ExpertA. The implementation will process this HIERARCHY block and add the files as specified above.

          Note

          Note the :latest addition. This specifies that the latest version of these files will be included. If needed, an organization can choose to pin it to a previous version. The entry can then be changed to BASE "platfohrm:3.0.2 and that ruleset will always be applied. Updates would not be automatic until the client updates the version, or specifies latest again.

          The entire file would look like this:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
          49
          50
          
          kind: policy-definition
          version: 1.0
          name: clientA default policy
          description: You can describe the policy here
          hierarchy:
            - base: "platfohrm:latest"
            - expert: "expertA:latest"
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd
          policies:
            # Contract level
            - for: parameter
              selector: "mealVoucher.value"
              when: "contract.Id" = "abcd"
              result:
                type: static
                value: 7.50
          
            # Employee level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.Id" = "1234"
              result:
                type: static
                value: 10.00
          
            # Department level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.department.name" = "sales"
              result:
                type: static
                value: 9.00
          
            # Establishment level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting" AND "employee.contract.location.city"
              result:
                type: static
                value: 6.00
          
            # Employer level
            - for: parameter
              selector: "mealVoucher.value"
              when: "employee.function.type" = "accounting"
              result:
                type: static
                value: 8.00
          Important

          Notice that the policies that are included in via the hierarchy are not present in this file. They will be added automatically. Here we focus on 1 file for a particular client.

          User Interface

          Dynamic UI

          1
          2
          3
          4
          5
          6
          7
          8
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              when: "contract.Id" = "abcd"
              result:
                type: entity
                selector: "com.platfohrm.employer.BikMobilePhone"
                where: "com.platfohrm.employer.Id" = "${{env.EmployerId}}"

          Our example above defines that the entity com.platfohrm.employer should have a field BikMobilePhone. The type of the field can be deduced from the parameter mobilePhone.benefitInKind. This definition allows the system to dynamically add this field to the UI responsible for the employer data. All that was needed is this one line policy definition.

          Working with policies

          In a first stage we will start from the file itself. Although in a later phase we will be able to define a UI interface to work with the policies. The input of a policy UI is always this file. It can be filtered to show the relevant policies in accordance with the subject on the screen. Let’s consider a UI for definition of policies related to components.

          Given this ruleset:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind"
              when: "contract.Id" = "abcd"
              result:
                type: static
                value: 3
                  
            - for: parameter
              selector: "mobileSubscription.benefitInKind"
              when: "contract.Id" = "abcd"
              result:
                type: static
                value: 3

          When the user has the component mobilePhone open, then the filter for the policies is:

          • All policies of type PARAMETER where name starts with mobilePhone.
          • (optional) filter by date range:
            • Active today: all that have no ON section or today’s date is between ON and TO of the date section

          Versioning

          Minimal policy

          For a client to start using the system, the minimum requirement is that a base is defined. The example bellow denotes the absolute minimum for the system to operate. In this example the client follows the latest version of the platfohrm base.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          
          kind: policy-definition
          version: 1.0
          name: clientA default policy
          description: You can describe the policy here
          hierarchy:
            - base: "platfohrm:latest"
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd
          policies: []

          Splitting policy sets

          Policy sets can become large, additionally we might want to define different defaults depending on a clients sector or wishes. We can achieve this by creating multiple files and combining them. Let’s apply the same principles for the most important levels and set this up.

          Defining a base

          Let’s say we want to provide a different base for construction oriented organizations. We will first create our base, one that we will use for all non-construction oriented organizations.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-base
          description: We recommend this policy for all non-construction oriented organizations.
          hierarchy: [] # Hierarchy is empty because we are defining a base
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mealVoucher.value"  # As an example we will provide a value for meal vouchers
              result:
                type: static
                value: 5.00
            - for: parameter
              selector: "mobilePhone.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3
            - for: parameter
              selector: "mobileSubscription.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3

          Now let’s define an entirely different base for construction oriented organizations.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-base-construction
          description: Base tailored to the construction sector
          hierarchy: [] # Hierarchy is empty because we are defining a base
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mealVoucher.value"  # As an example we will provide a value for meal vouchers
              result:
                type: static
                value: 8.00   # We change the value to 8
            - for: parameter
              selector: "mobilePhone.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3
            - for: parameter
              selector: "mobileSubscription.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3
          Caution

          Here we changed the entire ruleset to tailor for construction oriented organizations. We had to define unchanged policies as well. The problem with this is manageability. If we have to create a base for all sectors, we would have a lot of maintenance propagating our changes through all these different bases.

          A better way to achieve the same result but in a more manageable manner would be to extend our own default base. Doing so we would only need to define the changes for the construction instead of duplicating all policies. The result would be:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-base-construction
          description: Base tailored to the construction sector
          hierarchy: 
            - base: platfohrm-base:latest
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mealVoucher.value"  # As an example we will provide a value for meal vouchers
              result:
                type: static
                value: 8.00   # We change the value to 8

          Using this approach we would get all changes from the base. We just needed to define the changes.

          Note

          Note that we chose to follow the latest version of our platfohrm-base. The effect is that new versions would automatically impact our version for the construction sector. If we want to avoid that, all we need to do is change platfohrm-base:latest to platfohrm-base:1.0. That would give us even more control.

          A client can now choose what base to follow. The organization can define their base in their own policy definition:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          
          kind: policy-definition
          version: 1.0
          name: My organization
          description: You can describe the policy here
          hierarchy:
            - base: platfohrm-base-construction:latest
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd
          policies: []

          In this example, the organization decided to follow the platfohrm-base-construction and follow all changes automatically when new versions of the base are released.

          Cherry-picking

          Cherry-picking is the process where we can really fine-tune the needs of the policies. As an example, a car policy might be a difficult one. We as an organization want to offer more choices to the organizations using our software. Let’s say we want to define 2 car policies.

          Generous car policy:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-car-policy-generous
          description: Car policy allowing for a higher catalog price
          hierarchy: [] # Hierarchy is empty because we are defining a base
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "carPolicy.catalogPrice"  # As an example we will set a maximum for the catalog price
              result:
                type: static
                max: 36000   # We change the value to 36k

          Here we defined the generous car policy, we have set the maximum catalog price for a car to be 36 000.

          Budget car policy:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-car-policy-budget
          description: Budget friendly car policy
          hierarchy: [] # Hierarchy is empty because we are defining a base
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "carPolicy.catalogPrice"  # As an example we will set a maximum for the catalog price
              result:
                type: static
                max: 28000   # We change the value to 32k

          Here we have defined the budget car policy. We lowered the maximum catalog price to 28 000.

          We can now define our base in similar fashion. Since this base will be widely used, we will add the budget car policy to the base.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-base
          description: We recommend this policy for all non-construction oriented organizations.
          hierarchy:
            - expert: platfohrm-car-policy-budget:latest
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mealVoucher.value"  # As an example we will provide a value for meal vouchers
              result:
                type: static
                value: 8.00   # We change the value to 8
            - for: parameter
              selector: "mobilePhone.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3
            - for: parameter
              selector: "mobileSubscription.benefitInKind"  # We have other policies defined
              result:
                type: static
                value: 3

          Here we used our previous example of our base. We have now added the budget car policy to it. With cherry-picking we can go even further. Let’s create 2 more policies. One for meal vouchers and one for mobile phone.

          Meal vouchers:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-meal-vouchers
          description: Legal requirements when working with meal vouchers
          hierarchy: []
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mealVoucher.value"  # As an example we will provide a value for meal vouchers
              result:
                type: static
                min: 4.50   # We set the minimum to 4.5
                max: 10.00  # We set the maximum to 10

          We have set constraints on the meal vouchers. The policy above states that the value of meal vouchers needs to be between a min and a max: 4.50 and 10.00 respectively.

          Mobile phone:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-mobile-phone
          description: Mobile phone policy defaults
          hierarchy: []
          env: [] # No environment variables needed at this point
          policies:
            - for: parameter
              selector: "mobilePhone.benefitInKind" 
              result:
                type: static
                value: 3
            - for: parameter
              selector: "mobileSubscription.benefitInKind" 
              result:
                type: static
                value: 3

          Here we specify that the benefit in kind for a mobile phone always equals 3.00, and that the benefit in kind for a mobile subscriptionalso always equals3.00```.

          Combining all of the above, we can now specify our base policy in a very clean and maintainable way:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          
          kind: policy-definition
          version: 1.0
          name: platfohrm-base
          description: We recommend this policy for all non-construction oriented organizations.
          hierarchy:
            - expert: platfohrm-car-policy-budget:latest
            - expert: platfohrm-mobile-phone:latest
            - expert: platfohrm-meal-vouchers:latest
          env: [] # No environment variables needed at this point
          policies: []

          Here we have defined a base for organizations and experts to build upon. We cherry-picked policies and combined them to an all-inclusive policy base. An organization or an expert can now build upon this policy freely and add value to the policy as is needed. As a final example let’s say an expert might want to change the base policy and upgrade the car policy to the generous car policy. All he would have to do is define this in a policy file. The expert can define their own policies easily. AS an example let’s say the expert defines a policy for accounting oriented organizations.

          1
          2
          3
          4
          5
          6
          7
          8
          9
          
          kind: policy-definition
          version: 1.0
          name: expertA-accounting-policy-base
          description: We recommend this policy for all non-construction oriented organizations.
          hierarchy:
            - base: platfohrm-base:latest
            - expert: platfohrm-car-policy-generous:1.0
          env: [] # No environment variables needed at this point
          policies: []

          Here the expert created his own base. He started from the platfohrm-base but changed the car policy to the generous car policy. He also fixed the version of the car policy to 1.0 because he wanted more control over potential changes to provide even better support towards his clients. When he creates new clients in the system. The expert can now set this policy as a base for the client:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          
          kind: policy-definition
          version: 1.0
          name: My organization
          description: You can describe the policy here
          hierarchy:
            - base: expertA-accounting-policy-base:latest
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd
          policies: []

          The client will now follow the policy of expert A. Additional policies can be defined if needed, the client can start to tailor the policies to his needs. This completes the circle.

          Important

          Here the expert policy is set up as a base meaning that it inherits all defaults of the platfohrm-base. If this was not the case, the system would not work. But the expert is in control when creating the new client. He could have simple added the platfohrm-base to the policy definition of his client and achieve the same result.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          
          kind: policy-definition
          version: 1.0
          name: My organization
          description: You can describe the policy here
          hierarchy:
            - base: platfohrm-base:latest
            - base: expertA-accounting-policy-base:latest
          env:
            - EmployerId: 1234
            - EnvironmentId: abcd
          policies: []
          Note

          An expert is in control of his clients. And he can set up clients by defining policies. These policies can be tailored to the clients needs. With this approach we can allow experts to set up clients through a wizard, or allow experts to change configuration for clients. All fine-tuning is done through policies.

          Client provisioning

          Client provisioning is the process of seting up and deploying a container for a new client. In this project we will focus on self-service setup. The idea is that a client can set up new environments and has access to them via single sign on.

          Process

          CamundaClient provisioning

          img-client-provisioning.png img-client-provisioning.png

          The process needs to differentiate between a new environment and an existing one. The steps to create or update an environment differ.

          To create a new environment:

          • Deploy environment using ArgoCD and Kustomize
          • Create realm in Keycloak
          • Create Keycloak client for the frontend
          • Create identity providers
          • Configure the security headers
          • Create the admin user
          • Send password update mail to admin user

          When updating the environment:

          • Update the SMTP server
          • Configure Keycloak themes
          • Update locales
          • Update API cliens and roles

          Dynamic routes

          Dynamic routes are used to load layouts based on a structure in url format. These simplify the work needed to setup a page and allow for easy communication between dynamic components.

          Requirements

          • Fixed structure
          • Clean URL compliance wiki
          • Unlimited length
          • Component parameter support
          • Pass information up/down the component tree
          • Support commands

          Fixed structure

          Consider this url as an example:

          https://my-tool.org/employees/details/id/aaa-aaa-aaa/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc

          The url has the following parts:

          • host https://my-tool.org: this denotes the application url. It is the entry point into the application
          • layoutSection employees: used to group together layouts, and provides more context from a URL to the user
          • layoutName details: specifies that the details layout from the group employees should be loaded
          • data id/aaa-aaa-aaa/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc: The rest of the url is data that has no effect on the layout. This data will be passed down to the components.

          This gives us the following structure of URLs:

          <host>/<layoutSection>/<layoutName>[/<key>/<value>]*

          The key/value pairs are not required and the url can supply as many as is needed.

          We will omit the host part for the remainder of this document

          Implementation

          Loading the layout

          In the frontend a page will be created that captures the entire url. The page will load a dynamic layout from the backend given the layoutSection and layoutName.

          Important

          Considder this example: /employees. This url does not contain a layoutName section and loading would fail. A decision can be made to default the layout name to dashboard when none is provided. Only if this layout is not available from the backend will we redirect to the 404 page.

          Passing data

          The dynamic layout will then process the remainder of the URL. It will create a key/value list of the pairs in the url and pass it down to the layout itself.

          Considder our previous example: employees/details/id/aaa-aaa-aaa/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc

          This would load the layout details from employees section. It would then create this key/value list and pass it down:

          1
          2
          3
          4
          5
          6
          7
          
          {
            "layoutSection": "employees",
            "layoutName": "details",
            "id": "aaa-aaa-aaa",
            "account": "bbb-bbb-bbb",
            "transaction": "ccc-ccc-ccc"
          }

          Components from the dynamic layout can use this data as they please. The information would be available when needed.

          Important

          We include the layoutSection and layoutName in the data. This will be needed to load the correct service to handle requests and commands.

          Flexibility

          Using URLs this way make them very flexible. The following urls are all equal in how the UI reacts:

          • employees/details/id/aaa-aaa-aaa/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc
          • employees/details/account/bbb-bbb-bbb/transaction/ccc-ccc-ccc/id/aaa-aaa-aaa
          • employees/details/account/bbb-bbb-bbb/id/aaa-aaa-aaa/transaction/ccc-ccc-ccc

          As long as the key value pairs stay together the part after the layoutName will load the page correctly.

          Command pattern

          The dynamic URLs solve a lot of problems by allowing a command pattern through. Considder this url:

          employees/details/id/aaaa/do/delete

          The component that is responsible for loading id aaaa from the employees service receives an extra command parameter do. this signals the component to perform a delete action for the given id.

          At this point the record might be deleted, but the question becomes where do we go next? The layout can’t load this because the record with the given id no longer exists. We have a couple of ways to tackle this:

          • Default: decide on a default rule to load a layoutName list
          • Key/value: add data to the url and pass the desired destination
          Note

          A combination of these 2 will provide the most flexible implementation. Where the system redirects might be different depending on business logic. Therefor it is best to first check for additional data. If that is not present, redirect to the default layout. Redirecting would result in a 404 if the route can not be found and this is the desired behaviour.

          To redirect you can add parameters to the url/data:

          1. target-section: the section to redirect to after completion
          2. target-layout: the layout to redirect to for the given section
          Note

          if the target-section key is not defined, you can redirect to the original section.

          Processing actions

          To process an action, the first component that understands the action that was requested would execute it. The following steps would occur:

          1. Check if Do parameter exists
            1. Check security with the csrf token
            2. Execute the action
            3. Redirect either to the default layoutName or to the endpoint specified by the data
          2. Load the layout
            1. Parse the url
            2. Throw error or skip pairs on keys starting with _
            3. Load the requested layout

          Security

          The command pattern allows users to delete records through the url. Therefor we will implement a request forgery implementation. This will make sure that actions can only be triggered from within the UI.

          To achieve this action buttons need to do 2 things:

          1. Add a new GUID to the data object
          2. Add the GUID to the action URL

          For the component that executes the command the following actions are needed:

          1. Check for the token in the data object.
          2. If it exists, compare it with the token in the url
          3. If it is equal, do the requested action

          Upon failure, an error is shown to the user, and the layout does not refresh.

          So to implement, the action button adds an _csrfToken to the data:

          1
          2
          3
          4
          5
          6
          7
          8
          
          {
            "layoutSection": "employees",
            "layoutName": "details",
            "id": "aaa-aaa-aaa",
            "account": "bbb-bbb-bbb",
            "transaction": "ccc-ccc-ccc",
            "_csrfToken": "xxx-xxx-xxx"
          }

          Then the button can call the url employees/details/id/aaaa/do/delete/csrf/xxx-xxx-xxx. Only when the parameter csrf matches the _csrfToken in the data, will the action be executed.

          Important

          As a rule, the parameters parsed from the url can never start with a _! This would allow to bypass the security measures. Therefor the mechanism should throw an error when an underscore is found in one of the url parameter keys.