A lot of companies have an important requirement to validate documents from their users. It could be when a user is buying a plane ticket, registering on a cryptocurrency exchange, a bank, etc. This article illustrates how the document validation workflow could easily be implemented using Zenaton, without any crons, states machine, queuing system or modifications - just pure and simple code.

The following examples are presented in PHP, but are similar in other languages supported by Zenaton. If there is something you do not understand, please read the documentation and/or leave us a comment below.

documents validation

Use Case

  • A user signs up and opens an account. After entering the necessary information he is presented with the page where he must import his documents.
  • First, we are going to retrieve the name of the needed documents that he has to import according to his account type. We will assume for this example that he must import 2 documents, an ID and a proof of address.
  • After 3 days, if the user has not imported the documents, we send a reminder to tell him that he needs to import the remaining documents.
  • As we don’t want to be persistent, we’ll send a maximum of 3 reminders spaced 3 days apart.
  • After those reminders, if the user does not import his documents the account would not be opened, and we would email the user letting him know that he needs to sign in again and create the account.
  • When a user does import the document, we would check if the proof is valid or not.
  • If the documents are not valid, we would warn him by email with the given reason.
  • When all the documents are validated, we will notify the user that all proofs are valid.

The use case can be summed up in a flowchart as follows:

flowchart documents validation

Let’s dive in the code

After the user request, our web application will launch a DocumentValidationWorkflow with a requestId :

zenaton.run.workflow("DocumentValidationWorkflow", requestId);

The workflow class boilerplate

module.exports.handle = function* (requestId) {
    let remainingReminders = 3;
    let counter = requestId.documents.length;

    // workflow description 
};

module.exports.onEvent = function* (eventName, ...eventData) {
    // what to do when receiving an event
};

Waiting for validation

After retrieving the needed documents for this user request, the first action of this workflow is to wait until the user has imported the needed documents. For that, we use the Wait class provided by Zenaton.

module.exports = {
  *handle(requestId) {
    let remainingReminders = 3;
    let counter = requestId.documents.length;

    // Get the needed documents for this request
    yield this.run.task("GetRequestInformation", requestId);

    // While there are documents missing and the reminder limit hasn't been reached yet
    while (counter > 0 && remainingReminders > 0) {
      // Wait for a document to be uploaded for up to 3 days
      yield this.wait.event("ProofReceivedEvent").for(3 * 24 * 3600);

    }
  }
}

With the current implementation, we’ll wait indefinitely as we have not added a mechanism to complete the waiting of an event or decrease the number of remaining documents. This is why we are going to focus on the onEvent method.

Interact with events

When a user imports a document through the Web UI, it sends a DocumentImportedEvent with associated data.

<?php
use Zenaton\Interfaces\EventInterface;
class DocumentImportedEvent implements EventInterface
{
    public function __construct($type, $document)
    {
        $this->type = $type;
        $this->document = $document;
    }
}
<?php
$event = new DocumentImportedEvent("ID", "S3_URL_EXAMPLE");
DocumentValidationWorkflow::whereId($requestId)->send($event);

This event will be received by the onEvent method of our workflow:

  • When a user imports the document, we will check if the proof is valid or not.
  • If the document is not valid, we will warn him by email with the given reason.
<?php
...
    public function onEvent(EventInterface $event)
    {
      // When receiving an event,
      // Check that the received event is a DocumentImportedEvent
        if ($event instanceof DocumentImportedEvent) {
            switch ($event->type) {
                case "ID":
                    $response = (new CheckIDValidity($event->document))->execute();
                break;
                case "address_proof":
                    $response = (new CheckAdressProof($this->request->userId, $event->document))->execute();
                break;
            }
            if(! $response->valid){
                (new SendValidationRefusalEmail($this->request->userId, $response))->execute();
            } else {
                // Delete the valid document from the remaining documents list
                unset($this->documents[array_search($event->type, $this->documents, true)]);
            }
        }
    }

Now we handle events in our workflow, we send an email when the document is refused and delete the valid element from the remaining documents.

Reminders

Sending reminders to our user is super important for retention, but we also need to take care of our users and must not be too persistent, this is why we decide to:

  • After 3 days, send a reminder to the user to tell him that he needs to import the remaining documents.
  • We’ll send a maximum of 3 reminders spaced 3 days apart.
  • After those reminders, if the user did not import his documents we will close the account by sending him an email.
module.exports = {
  *handle(requestId) {
    let remainingReminders = 3;
    let counter = requestId.documents.length;

    // While there are documents missing and the reminder limit hasn't been reached yet
    while (counter > 0 && remainingReminders > 0) {
      // Wait for a document to be uploaded for up to 3 days
      const event = yield this.wait.event("ProofReceivedEvent").for(3 * 24 * 3600);

      // If a document is received
      if(event) {
        // Get the data of the event
        const [eventName, eventData] = event;
        // to know if the document is valid and update the counter of remaining documents.
        if (eventData.isValid) {
          counter--;
        }
      } else {
        // If no document has been uploaded within 3 days
        // Send an email reminder to the user
        yield* this.sendEmailReminder(requestId, remainingReminders);
        remainingReminders--;
      }
    }

    // Notify the user via email using the Sendgrid API connector
    // about the registration process completion
    // depending on the number of valid documents.
    // If all documents are valid
    if (counter === 0) {
      // Notify the user that the registration process is complete
      yield* this.sendEmailRegistrationComplete(requestId);
    } else {
      // Notify the user that the registration process failed
      yield* this.sendEmailRegistrationFailed(requestId, remainingReminders);
    }
  }
}

Final implementation

module.exports = {
  *handle(requestId) {
    let remainingReminders = 3;
    let counter = requestId.documents.length;

    // While there are documents missing and the reminder limit hasn't been reached yet
    while (counter > 0 && remainingReminders > 0) {
      // Wait for a document to be uploaded for up to 3 days
      const event = yield this.wait.event("ProofReceivedEvent").for(3 * 24 * 3600);

      // If a document is received
      if(event) {
        // Get the data of the event
        const [eventName, eventData] = event;
        // to know if the document is valid and update the counter of remaining documents.
        if (eventData.isValid) {
          counter--;
        }
      } else {
        // If no document has been uploaded within 3 days
        // Send an email reminder to the user
        yield* this.sendEmailReminder(requestId, remainingReminders);
        remainingReminders--;
      }
    }

    // Notify the user via email using the Sendgrid API connector
    // about the registration process completion
    // depending on the number of valid documents.
    // If all documents are valid
    if (counter === 0) {
      // Notify the user that the registration process is complete
      yield* this.sendEmailRegistrationComplete(requestId);
    } else {
      // Notify the user that the registration process failed
      yield* this.sendEmailRegistrationFailed(requestId, remainingReminders);
    }
  }
}

It took me a very short time to implement this workflow logic from the idea to the current implementation. Doing this without Zenaton would have required a lot more time and completing chores like database migrations, crons, queue etc.

Thanks to Zenaton dashboard, I am also able to monitor and debug in case of failures the workflow.

As soon as a customer registers and opens an account, the workflow is triggered.

real time executions workflow

Zenaton is for technical teams that understand that their primary mission is to improve the business through quick iterations and new ideas, not spending most of their time solving purely technical issues. If you have additional questions (or other use cases), feel free to contact me at louis at zenaton.com or to ask them below 👇