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 :

(new DocumentValidationWorkflow($requestId))->dispatch();

The workflow class boilerplate

<?php
use Zenaton\Interfaces\EventInterface;
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;
class DocumentValidationWorkflow implements WorkflowInterface
{
    use Zenatonable;
    protected $requestId;
    protected $remainingReminders = 3;
    protected $documents = [];
    public function __construct($requestId)
    {
        $this->requestId = $requestId;
    }
    public function handle()
    {
        // The Workflow logic
    }
    
    public function onEvent(EventInterface $event)
    {
        // Method to interract with incoming events 
    }
    public function getId()
    {
        // Use to identify a workflow instance
        return $this->requestId;
    }
}

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.

<?php
...
    public function handle()
    {
        // Get the needed documents for this request
        $request = (new GetRequestInformation($this->requestId))->execute();
        
        // Store the list of documents
        $this->documents = $request->documents;
        
        while(count($this->documents) > 0){
            (new Wait(DocumentImportedEvent::class))->execute();
        }
    }

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.
<?php
...
    public function handle()
    {
        ...
        while(count($this->documents) > 0 && $this->remainingReminders > 0){
            
            $event = (new Wait(ProofReceivedEvent::class))->days(3)->execute();
            
            if( ! $event ) {
                // Send a reminder with the list of documents to provide
                (new SendReminderEmail($request))->dispatch();
                $this->remainingReminders--;
            }
        }
    
        if(count($this->documents) === 0){
            // Success path
            (new SendSuccessValidationEmail($request))->dispatch();
        else {
            // Close validation
            (new SendFailureValidationEmail($request))->dispatch();
        }
    }

Final implementation

<?php
use Zenaton\Interfaces\EventInterface;
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;
class DocumentValidationWorkflow implements WorkflowInterface
{
    use Zenatonable;
    protected $requeastId;
    protected $remainingReminders = 3;
    protected $documents = [];
    public function __construct($requestId)
    {
        $this->requestId = $requestId;
    }
    public function handle()
    {
        // Get the needed documents for this request
        $request = (new GetRequestInformation($this->requestId))->execute();
        // Store the proofs
        $this->documents = $request->documents;
        while (count($this->documents) > 0 && $this->remainingReminders > 0) {
            $event = (new Wait(ProofReceivedEvent::class))->days(3)->execute();
            if (!$event) {
                // Send a reminder with the list of documents to provide
                (new SendReminderEmail($request))->dispatch();
                --$this->remainingReminders;
            }
        }
        if (count($this->documents) === 0) {
            // Success path
            (new SendSuccessValidationEmail($request))->dispatch();
        } else {
            // Close validation
            (new SendFailureValidationEmail($request))->dispatch();
        }
    }
    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 documents list
                unset($this->documents[array_search($event->type, $this->documents, true)]);
            }
        }
    }
    public function getId()
    {
        return $this->requestId;
    }
}

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](https://zenaton.com) 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 👇