Introducing Self-Serve Plan Changes for Legacy Hootsuite Members

Introducing Self-Serve Plan Changes for Legacy Hootsuite Members
Introducing Self Serve Plan Changes for Legacy Hootsuite Members

Before the overage page loads, Ajax calls are made from the React app to other services to retrieve the feature usage summary, and if necessary, organization members and social network lists. This data is required to populate the lists with items to select from in the management boxes.

Clicking confirm POSTs the list of plan changes, including the desired base plan, legacy add-ons to be removed, and the requested billing interval (annual or monthly). Billing Service pipes the changes through the rules engine, updates records in the database, and then propagates them to the payment platform, so that the user will be prorated/charged according to their selections.

An example change set from a Pro user changing plans.

However, the rules engine first needed to be updated to allow the transition from Legacy Pro to occur immediately. The rules engine is a class that applies a list of rules to requested account updates to dictate the timing and proration policies. Proration is the act of charging or refunding the customer a partial amount of the subscription cycle. For example, a new user in a trial can downgrade their account to Free immediately, but one not in trial trying to do the same transition will have the downgrade performed at the end of their billing cycle. A rule is made of a matching predicate, scope predicate, and a set of policies to add. A predicate is simply a condition that the engine checks against the contents of the account change request.

Below is the rule enacted upon to prorate a user moving away from Pro:

ProductPredicate((from = Some(ProductCode.PRO)))
ProductPredicate((to = Some(ProductCode.FREE)))
IntervalPredicate((from = Some(Annual)), (to = Some(Monthly)))
Set[Policy](AlignmentPolicy(Immediate), ProrationPolicy(Credit))

The conjunctive predicate joins the three predicates within, so all conditions must match for the rule to apply. The rule will match changes from Legacy Pro to a paid plan, with billing intervals other than annual to monthly. The scoping tautological predicate always returns true, which means that the rule will apply to all other changes in the change set. The policy set contains the directives to apply to the change set. For example, a removal of a Legacy Pro specific add-on as a result of the transition will also be immediate and produce a refund in the form of credit.

What the rule engine does is that it folds over the list of rules, and applies the policies of matching ones to the change set, as long as that policy category has not already had a match. This way, we can prioritize rules based on order. A set of default rules are saved for the end, in case no custom rules match at all.

rules.foldLeft((Map.empty[PolicyType, Policy], Seq[Rule]())) {
case (
(map, remainingRules),
Rule(predicate, policies, scopePredicate)
if anyOperationsMatchPredicate(operations, predicate) &&
scopePredicate == TautologicalPredicate =>
reducePolicies(policies.toSeq, map) -> remainingRules
case ((map, remainingRules), rule) =>
map -> (remainingRules :+ rule)

Then the rules engine repeats the process, but with rules that are not tautologically scoped and thus only apply to some parts of the change set. These scoped rules override policies set from the base rules.

Source link