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 $request
object:
(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 theWait
instruction in the mainhandle
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 👇