~/dominik.polzer cd ./About

Blocking Rule Engine


The first challenge was creating an abstraction compatible with both the Functional C# API and the object-oriented API that worked with the standard library exceptions.

An important detail was that the Provider API also needed to wrap the existing composition to handle dependency injection (DI / Lifetime scoping of dependent services) properly.

Functional & object oriented Api

This also involved propagating custom Blocking logic errors through both GraphQL middleware as well as HTTP middleware to be shown in the query responses.

The HTTP middleware was modified only because the product owners wanted custom HTTP error codes for these non-standard GraphQL responses (we used 403 as default, we also had different response codes for Exceptions thrown from other parts of the system).

Why custom errors?

We decided to add custom errors to the Validation Engine because there are quite a few possible combinations, making it easier to trace back and revalidate the exact Rule that triggered the error.

I also included Tags in the response payloads, allowing us to get a more granular view of the specific case (combination of rule attributes) that blocked the candidate assignment.

Example of custom response thrown by the Blocking Rule Engine

   "data": [
            {
                "failureReason": "business_rule",
                "message": "A shift that starts within 60 minutes cannot be self-cancelled, please call your Local Office for assistance.",
                "tag": "Any CandidateStatusId",
                "success": false
            }
        ],
 //...

Another part of the challenge involved working through and thoroughly testing all possible and realistic configuration combinations with the QA teams. This turned out to be a lengthy process, spanning a few weeks when accounting for all the implemented rules up to that point.

It also required several syncs with QA and PO domain experts to properly configure the desired states and preconditions before calling the GraphQL API Mutations.

Here are a few examples of the Rules that were implemented at that point:

Each rule definition bellow includes a single rule configuration as an example, but in reality, there are multiple rows for each rule.

Blocking Rule Grouping config was structured like this.

<BusinessRules>
	<Groups>
		<BusinessRulesGroup AboutType="Shift">
			<BusinessRule Type="SideJobPrevention" Enabled="True">
            <Rule ...>
            ... more rules 
         </BusinessRule>
	... more groups
</BusinessRules>

Assign and Cancel Shift Bookings

Assign and Cancel Shift Bookings

<Rule ForCandidateStatusIds="7" Minutes="120" Enforce="true"/>

Description:

As a Candidate assigned with a certain status, when I try to Book or Cancel a Shift in a certain status, once the threshold time limit passes I should be given an error that action I did was not allowed because of the enforced rule.

Consecutive Bookings PerXDays Prevention

ConsecutiveBookingsPerXDaysPrevention Rule

<Rule ForCandidateStatusId="50" ForShiftStatusId="116" PeriodInDays="7" TimesBookingIsAllowed="5" Enforce="true">

Description:

As a Candidate assigned with a certain status, when I try to Book a Shift in a certain status, in certain period of days, 1 time over the enforced threshold, I should be given an error that action I did was not allowed because of the enforced rule.

ExhaustionPrevention

ExhaustionPrevention Rule

<Rule ForCandidateStatusId="48" ForShiftStatusId="116" HoursAllowed="12" InLastXHours="24" Enforce="true">

Description:

As a Candidate assigned with a certain status, when I try to Book consecutive Shifts in a certain status that together last X number of hours, in enforced Y period of hours, I should be given an error that action I did was not allowed because of the enforced rule.

Code (both backwards and forwards passes in the single query)

var shiftsInRelevantDateTimeRange = (
    from shift in ShiftRepo.All
    join match in Matches.All on shift.MatchId equals match.Id
    where match.CandidateId == prms.CandidateId
                //Both before & after WithinXHours ranges combined
                && shift.StartTime >= DbFunctions.AddHours(newShiftEndDateTime, -shortestWithinXHours)
                && shift.StartTime <= DbFunctions.AddHours(newShiftStartDateTime, shortestWithinXHours)
                && shift.StatusID == 1113 //Assigned
                && shift.CompanyDepartment.CompanyID != assignmentCompanyId
    select shift).ToList();

var forwardsSum = shiftsInRelevantDateTimeRange.Where(s => s.StartTime >= newShiftStartDateTime
    && s.StartTime <= DbFunctions.AddHours(newShiftStartDateTime, shortestWithinXHours))
    .Sum(x => DbFunctions.DiffHours(x.StartTime, x.EndTime) ?? 0);

var backwardsSum = shiftsInRelevantDateTimeRange.Where(s => s.StartTime >= DbFunctions.AddHours(newShiftEndDateTime, -shortestWithinXHours)
    && s.StartTime <= newShiftEndDateTime)
    .Sum(x => DbFunctions.DiffHours(x.StartTime, x.EndTime) ?? 0);

if (forwardsSum > shortestHoursAllowed || backwardsSum > shortestHoursAllowed) 
    ...match & validate rule attributes

IndecisivePrevention

<Rule ForCandidateStatusId="36" IfShiftEndReasonIds="2" ForTheNextXDays="1" Enforce="True"/>

Description:

As a Candidate assigned with a certain status, when I try to Book a Shift at Facility 2, after the fact I cancelled a shift at Facility 1, if time threshold ForTheNextXDays didn’t pass, I should be given an error that action I did was not allowed because of the enforced rule.

SideJobPrevention

<Rule ForCandidateStatusId="858" RoleTypeId="3" Enforce="True" />

Description:

As a Candidate with an active Contract Job match to which I am assigned with a certain status, when I attempt to Book a shift prior to the end of that Contract Job, I should be shown an error message that indicates that Booking Shifts while on an Active Contract Job that overlap with the shift start date is not allowed.

This was just the front gate into the actual Candidate Assignment …

In practice, I hooked into an existing GraphQL mutation, AssignCandidate, which already had its own validation and executed several SQL transactions after passing the Blocking Rule validation.

Assign candidate Mutation flow

Dominik Polzer