marketplace workflow background jobs

The most important feature of a marketplace is how it organizes communications between sellers and buyers. This article illustrates how it can easily be implemented using Zenaton, without any cron jobs, or database requests or modifications — just pure & 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.

Use Case

Let's assume we operate a marketplace where users post a request for a service and several providers can bid and provide this service. We will make some assumptions about how our marketplace works:

  • first, a user makes a request for a service
  • this request is reviewed by a moderator
  • if the request is rejected, we notify the user and explain why
  • if not, we select a few providers we think can handle the users request
  • we notify each of those providers of this new request
  • we wait for at least 3 quotes over the course of 3 days maximum
  • after more than 3 quotes, we warn the provider that it’s too late
  • if we don’t have any quotes after 3 days, we tell the user their request cannot be fulfilled
  • after 3 days or as soon as we have 3 quotes, we send them to the user
  • we wait for the user to choose one of quotes
  • we notify the chosen provider and let the other providers know that they were not chosen.

Moderation

After the user request, our web application will launch a RequestManagementWorkflow with the $requestobject:

(new RequestManagementWorkflow($request))->dispatch(); 

The first action of this workflow is to wait for moderation. For that, we use the Wait class provided by Zenaton.

<?php
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;
class RequestManagementWorkflow implements WorkflowInterface
{
    use Zenatonable;
    protected $request;
    public function __construct($request)
    {
        $this->request = $request;
    }
      
    public function getId()
    {
        return $this->request->id;
    }
    
    public function handle()
    {
        // wait for moderation
        $event = (new Wait(RequestModerationEvent::class))->execute();
        // if rejected, tell user
        if ($event->rejected) {
            (new SendRejectionNotification($this->request, $event->reason))->execute();
            return;
        }
        
        ...
    }

The moderator has a UI listing all requests and decides what to do with each. According to their decision, moderation application will send a RequestModerationEvent to RequestManagementWorkflow

$event = new RequestModerationEvent(true, 'missing description');
RequestManagementWorkflow::whereId($request->id)->send($event);

RequestModerationEvent class has 2 public properties, rejected and reason. Following the above workflow implementation, if $event->rejected is true then we will send a rejection notification to the user and end there.

Select and notify providers

Then we execute a SelectProvidersForRequest task that will return an array of providers objects. For each of them, we will notify the provider by dispatching a NotifyProviderOfNewRequest task. We use a dispatch (instead of execute) here as there is no need to execute these tasks sequentially. All providers can be notified at once.

<?php
...
    public function handle()
    {
        ...
        // select a set of providers for this request
        $providers = (new SelectProvidersForRequest($this->request))->execute();
        // notify these providers
        foreach ($providers as $provider) {
            (new NotifyProviderOfNewRequest($provider, $this->request))->dispatch();
        }
        ...
    }

Waiting for quotes

Each time a provider enters a quote through the provider interface, a ProviderQuotationEvent will be sent to this workflow,

$event = new ProviderQuotationEvent($provider, $quotation);RequestManagementWorkflow::whereId($request->id)->send($event);

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

  • if enough quotes have already been received, we notify the provider that it’s too late
  • if not, we add the quote to the $this->quotations array
  • if enough quotes have been received, we send an ProvidersQuotationGatheredEvent to itself to release the Wait instruction in the main handle method.
<?php
...
    public function handle()
    {
        ...
        // wait for at most 3 providers quotation, 3 days maximum
        $event = (new Wait(ProvidersQuotationGatheredEvent::class))->days(3)->execute();
        ...
    }
    public function onEvent($event)
    {
        if ($event instanceof ProviderQuotationEvent) {
            // tell provider it's too late, if we already have enough quotations
            if (count($this->quotations) >= self::ENOUGH_PROVIDERS) {
                (new NotifyProviderItsTooLate($event->provider, $event->quotation))->execute();
                return;
            }
            // update or add provider quotation in quotations array
            $this->quotation[$event->provider->id] = $event->quotation;
            // if enough quotation, continue the main flow
            if (count($this->quotations) >= self::ENOUGH_PROVIDERS) {
                // send an event to itself
                self::whereId($this->getId())->send(new ProvidersQuotationGatheredEvent());
            }
        }
    }
...

Wait for user choice and notify providers

  • If no quote has been received, then we notify the user that they’ve received no response, otherwise we send all quotes to the user
  • Then we wait for the user to choose a quote
  • and finally we notify the providers of the user’s choice
<?php
...
    public function handle()
    {
        ...
        // if no response received in 3 days
        if (0 == count($this->quotations)) {
            (new NotifyUserOfNoResponse($this->request))->execute();
            return;
        }
        // notify quotation to user
        (new NotifyQuotationsToUser($this->request, $this->quotations))->execute();
        // wait user choice
        $event = (new Wait(QuotationChosenByUserEvent::class))->execute();
        // notify user choice to providers        
        foreach ($this->quotations as $providerId => $quotation) {
            if ($providerId == $selected) {
                (new NotifiyChosenProvider($providerId, $this->request))->dispatch();
            } else {
                (new NotifiyNotChosenProvider($providerId, $this->request))->dispatch();
            }
        }
    }

Final implementation

The complete implementation is below:

<?php
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;
class RequestManagementWorkflow implements WorkflowInterface
{
    use Zenatonable;
    const ENOUGH_PROVIDERS = 3;
    protected $request;
    protected $quotations;
    public function __construct($request)
    {
        $this->request = $request;
        $this->quotations = [];
    }
    public function getId()
    {
        return $this->request->id;
    }
    
    public function handle()
    {
        // wait for moderation
        $event = (new Wait(ModerationEvent::class))->execute();
        // if rejected, tell user
        if ($event->rejected) {
            (new SendRejectionNotification($this->request, $event->reason))->execute();
            return;
        }
        // select a set of providers for this request
        $providers = (new SelectProvidersForRequest($this->request))->execute();
        // notify these providers
        foreach ($providers as $provider) {
            (new NotifyProviderOfNewRequest($provider, $this->request))->dispatch();
        }
        // wait for at most 3 providers quotation, 3 days maximum
        $event = (new Wait(ProvidersQuotationGatheredEvent::class))->days(3)->execute();
        // if no response received in 3 days
        if (0 == count($this->quotations)) {
            (new NotifyUserOfNoResponse($this->request))->execute();
            return;
        }
        // notify quotation to user
        (new NotifyQuotationsToUser($this->request, $this->quotations))->execute();
        // wait user choice
        $event = (new Wait(QuotationChosenByUserEvent::class))->execute();
        // notify user choice to providers        
        foreach ($this->quotations as $providerId => $quotation) {
            if ($providerId == $event->provider->id) {
                (new NotifiyChosenProvider($providerId, $this->request))->dispatch();
            } else {
                (new NotifiyNotChosenProvider($providerId, $this->request))->dispatch();
            }
        }
    }
    public function onEvent($event)
    {
        if ($event instanceof ProviderQuotationEvent) {
            // tell provider it's too late, if we already have enough quotations
            if (count($this->quotations) >= self::ENOUGH_PROVIDERS) {
                (new NotifyProviderItsTooLate($event->provider, $event->quotation))->execute();
                return;
            }
            
            // update or add provider quotation in quotations array
            $this->quotation[$event->provider->id] = $event->quotation;
            // if enough quotation, continue the main flow
            if (count($this->quotations) >= self::ENOUGH_PROVIDERS) {
                // send an event to itself
                self::whereId($this->getId())->send(new ProvidersQuotationGatheredEvent());
            }
        }
    }
}

It took me just a few hours to implement this class (without tasks implementation). Doing this without Zenaton, with cron and states variables, would probably have taken me days or perhaps weeks…not to mention the time to debug and test.

What’s more, updating this workflow is really easy — eg. we can add some timeouts for the user choice or manage the case in which the user doesn’t choose any quotes. With Zenaton, it’s becoming very easy to improve your business workflows :)

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 gilles at zenaton.com or to ask them below 👇