Table of Contents

Payment Processor Example

This is a simple example to demonstrate how to create a straightforward step-by-step workflow. We'll then add error reporting in case one of the steps fails.

In a typical payment processor, there are generally two workflows:

  1. Payment intake
  2. Payment batch transmission

Creating a new payment requires validating the incoming data and storing the payment. Later, the transmission piece will be responsible for batching payment together in a NACHA file and communicating that file to a bank (assuming we're doing ACH payments). The details of the validation, storage, NACHA creation, and bank communication are all beyond the scope of this article. Since we're looking at JobFlow, we're going to look at how to easily link all these steps together into an asynchronous, scalable system.

Payment Intake

For both of these tasks (intake and batching) the initial rule creation is straightforward. The rules simply designate steps in the flow.

[FlowRule("Payment")]
public class ValidatePaymentRule : JobFlowRule<PaymentModel>
{
    protected override void DefineRule()
    {
        When()
            .IsStart();

        Then()
            .StartJob("ValidatePayment");
    }
}

[FlowRule("Payment")]
public class CreatePaymentRule : JobFlowRule<PaymentModel>
{
    protected override void DefineRule()
    {
        When()
            .HaveJob("ValidatePayment", JobResponseStatus.Success);

        Then()
            .StartJob("CreatePayment");
    }
}

As we can see, these rules move the item (a payment) from one step to the next with little intervention.

If the creation of the payment in the system fails, that would likely indicate a system exception of some sort (database connections problems, for example). As far as the application user is concerned, the validation steps should take care of any issues the user may be concerned about - invalid payees, amounts, etc.

In the CreatePaymentRule, we're matching the previous step's result code that we want to take into account. CreatePaymentRule will only execute if the previous step's result (ValidatePayment) is Success. Now, let's create a rule in case it's not successful.

[FlowRule("Payment")]
public class ReportValidationFailureRule : JobFlowRule
{
    public void DefineRule()
    {
        When()
            .HaveJob("ValidatePayment", JobResponseStatus.Failure);

        Then()
            .StartJob("ReportValidationError");
    }
}

For this rule, we're looking for a result of Failure, creating a job that's responsible for reporting that validation failure. A question, however, is what do we do with the validation failures themselves - what errors do we communicate back to the user? Currently, the handlers in the system are meant to be "pure" in that they don't directly modify the work item data. Therefore, the rules themselves must handle updating work item data.

Note: When using the SDK, there's nothing stopping the handler from accessing the document storage directly. That does mean adding connection information to each handler accessing the data.

In our payment processor case, when we receive validation failures, we'll store them with the payment data before moving to the ReportValidationErrors step. Jobs are able to return data back to the rules by setting Data property on the WorkResponse. To make use of the result data, we can make use of a different JobFlowRule base class.

[FlowRule("Payment")]
public class ReportValidationFailureRule : JobFlowRule<PaymentModel>
{
    protected override void DefineRule()
    {
        WorkResponse response = null;
        Document<PaymentModel> document = null;
        When()
            .HaveJob("ValidatePayment", 
                _ => _.WithResponse(() => response, _ => _.WithStatus(WorkResponseStatus.Failure)))
            .HaveDocument(() => document);

        Then()
            .StartJob("ReportValidationError")
            .Do(_ => Action(document, response, Logger, _.Factories));
    }

    protected void Action(Document<PaymentModel> currentDocument, WorkResponse currentResponse, ILogger<JobFlowRule> logger, IJobFlowFactories flowRepository)
    {
        var resultModel = currentResponse.ResponseData.ToObject<ValidationResultModel>();
        currentDocument.Data.ValidationResult = resultModel;
        flowRepository.Documents.Update(currentDocument);
        
        logger.LogError($"Validation Error: {resultModel.Message}");
    }
}

This JobFlowRule base class takes a generic parameter which indicates the document (work item) data type. In our case, a PaymentModel. Now, our action method has access to more classes and managers to allow updating the work item document data and propagating the changes down to the document storage. When the ReportValidationError handler executes, the document data will have those validation errors available. Note that the Data property is a JObject and may be converted into a concrete type. This allows the core system to operate without having to know about any of our custom DTOs being send in the messages.

You're not required to update the system's work item data in this way. Your system may be designed in a way that all the pertinent data is stored in your own storage solution. In that case, the work item document may only consist of an identifier to tell the rest of your system where to load the data.

Note: Because the engine uses NRules for defining the rules, the entire NRules DSL is available to your code.

Payment Transmission

Payment transmission uses a similar set of rules.

[FlowRule("TransmitPayments")]
public class BatchPaymentsRule : JobFlowRule
{
    protected override void DefineRule()
    {
        When()
            .HaveJob(_ => _.WithNames(GlobalJobNames.Start));

        Then()
            .StartJob("BatchPayments");
    }
}
[FlowRule("TransmitPayments")]
public class CreateNachaRule : JobFlowRule
{
    protected override void DefineRule()
    {
        When()
            .HaveJob(
                _ => _.WithNames("BatchPayments")
                .WithResponse(_ => _.WithStatus(WorkResponseStatus.Success)));

        Then()
            .StartJob("CreateNacha");
    }
}
[FlowRule("TransmitPayments")]
public class TransmitNachaRule : JobFlowRule
{
    protected override void DefineRule()
    {
        When()
            .HaveJob(
                _ => _.WithNames("CreateNacha")
                .WithResponse(_ => _.WithStatus(WorkResponseStatus.Success)));

        Then()
            .StartJob("TransmitNacha");
    }
}

Since this is a behind-the-scenes process, we don't necessarily need to handle any "user friendly" error messaging. We can allow the system's standard error reporting kick in. However, there are a couple other scenarios that would might need to worry about. For one, storing the entire NACHA file in the payment data document might be overkill. Instead, the system provides an Attachment model for those scenarios.

Also, what happens if the transmission fails due to a transient error? For example, if the bank's FTP server doesn't respond in time, we don't want the entire process to fail. JobFlow handles retrying jobs for this reasons. In another section, we'll look at how you can schedule a job to retry later.