From ee192bf0730a04c03393f02a7baaab15f36a356f Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap@edeveloper.nl>
Date: Tue, 28 Apr 2015 13:43:13 +0200
Subject: [PATCH] added delay to civirules actions

---
 CRM/Civirules/DAO/RuleAction.php              |   5 +
 CRM/Civirules/Delay/Delay.php                 |  84 ++++++++++++
 CRM/Civirules/Delay/Factory.php               |  56 ++++++++
 CRM/Civirules/Delay/XDays.php                 |  34 +++++
 CRM/Civirules/Delay/XWeekDay.php              | 110 ++++++++++++++++
 CRM/Civirules/Delay/XWeekDayOfMonth.php       |  97 ++++++++++++++
 CRM/Civirules/Engine.php                      | 121 +++++++++++++++++-
 CRM/Civirules/Form/Rule.php                   |   6 +
 CRM/Civirules/Form/RuleAction.php             |  31 ++++-
 CRM/Queue/Queue/Civirules.php                 |  21 +++
 api/v3/CiviRuleAction/Process.mgd.php         |  20 +++
 api/v3/CiviRuleAction/Process.php             |  19 +++
 sql/createCiviruleRuleAction.sql              |   1 +
 templates/CRM/Civirules/Delay/XDays.tpl       |   3 +
 templates/CRM/Civirules/Delay/XWeekDay.tpl    |   6 +
 .../CRM/Civirules/Delay/XWeekDayOfMonth.tpl   |   6 +
 templates/CRM/Civirules/Form/RuleAction.tpl   |  36 +++++-
 .../Civirules/Form/RuleBlocks/ActionBlock.tpl |   4 +
 xml/Menu/civirules.xml                        |   2 +-
 19 files changed, 658 insertions(+), 4 deletions(-)
 create mode 100644 CRM/Civirules/Delay/Delay.php
 create mode 100644 CRM/Civirules/Delay/Factory.php
 create mode 100644 CRM/Civirules/Delay/XDays.php
 create mode 100644 CRM/Civirules/Delay/XWeekDay.php
 create mode 100644 CRM/Civirules/Delay/XWeekDayOfMonth.php
 create mode 100644 CRM/Queue/Queue/Civirules.php
 create mode 100644 api/v3/CiviRuleAction/Process.mgd.php
 create mode 100644 api/v3/CiviRuleAction/Process.php
 create mode 100644 templates/CRM/Civirules/Delay/XDays.tpl
 create mode 100644 templates/CRM/Civirules/Delay/XWeekDay.tpl
 create mode 100644 templates/CRM/Civirules/Delay/XWeekDayOfMonth.tpl

diff --git a/CRM/Civirules/DAO/RuleAction.php b/CRM/Civirules/DAO/RuleAction.php
index 5571ce7..cefa3db 100755
--- a/CRM/Civirules/DAO/RuleAction.php
+++ b/CRM/Civirules/DAO/RuleAction.php
@@ -46,6 +46,10 @@ class CRM_Civirules_DAO_RuleAction extends CRM_Core_DAO {
           'name' => 'action_params',
           'type' => CRM_Utils_Type::T_TEXT
         ),
+        'delay' => array(
+          'name' => 'delay',
+          'type' => CRM_Utils_Type::T_TEXT
+        ),
         'is_active' => array(
           'name' => 'is_active',
           'type' => CRM_Utils_Type::T_INT,
@@ -68,6 +72,7 @@ class CRM_Civirules_DAO_RuleAction extends CRM_Core_DAO {
         'rule_id' => 'rule_id',
         'action_id' => 'action_id',
         'action_params' => 'action_params',
+        'delay' => 'delay',
         'is_active' => 'is_active'
       );
     }
diff --git a/CRM/Civirules/Delay/Delay.php b/CRM/Civirules/Delay/Delay.php
new file mode 100644
index 0000000..dcfbf58
--- /dev/null
+++ b/CRM/Civirules/Delay/Delay.php
@@ -0,0 +1,84 @@
+<?php
+
+abstract class CRM_Civirules_Delay_Delay {
+
+  /**
+   * Returns the DateTime to which an action is delayed to
+   *
+   * @param DateTime $date
+   * @return DateTime
+   */
+  abstract public function delayTo(DateTime $date);
+
+  /**
+   * Add elements to the form
+   *
+   * @param \CRM_Core_Form $form
+   * @return mixed
+   */
+  abstract public function addElements(CRM_Core_Form &$form);
+
+  /**
+   * Validate the values and set error message in $errors
+   *
+   * @param array $values
+   * @param array $errors
+   * @return void
+   */
+  abstract public function validate($values, &$errors);
+
+  /**
+   * Set the values
+   *
+   * @param array $values
+   * @return void
+   */
+  abstract public function setValues($values);
+
+  /**
+   * Returns an description of the delay
+   *
+   * @return string
+   */
+  abstract public function getDescription();
+
+  /**
+   * Returns an explanation of the delay
+   *
+   * @return string
+   */
+  public function getDelayExplanation() {
+    return $this->getDescription();
+  }
+
+  /**
+   * Set default values
+   *
+   * @param $values
+   */
+  public function setDefaultValues(&$values) {
+
+  }
+
+  /**
+   * Returns the name of the template
+   *
+   * @return string
+   */
+  public function getTemplateFilename() {
+    return str_replace('_',
+        DIRECTORY_SEPARATOR,
+        CRM_Utils_System::getClassName($this)
+      ) . '.tpl';
+  }
+
+  /**
+   * Returns the name
+   *
+   * @return string
+   */
+  public function getName() {
+    return get_class($this);
+  }
+
+}
\ No newline at end of file
diff --git a/CRM/Civirules/Delay/Factory.php b/CRM/Civirules/Delay/Factory.php
new file mode 100644
index 0000000..d86df66
--- /dev/null
+++ b/CRM/Civirules/Delay/Factory.php
@@ -0,0 +1,56 @@
+<?php
+
+class CRM_Civirules_Delay_Factory {
+
+  /**
+   * Get a list with all possible delay classes
+   *
+   * @return array
+   */
+  public static function getAllDelayClasses() {
+    return array(
+      new CRM_Civirules_Delay_XDays(),
+      new CRM_Civirules_Delay_XWeekDay(),
+      new CRM_Civirules_Delay_XWeekDayOfMonth(),
+    );
+  }
+
+  /**
+   * Returns the delay class for a given name
+   *
+   * @param $name
+   * @return CRM_Civirules_Delay_Delay
+   * @throws Exception
+   */
+  public static function getDelayClassByName($name) {
+    foreach(self::getAllDelayClasses() as $class) {
+      if ($class->getName() == $name) {
+        return $class;
+      }
+    }
+
+    throw new Exception('Could not find delay class for '.$name);
+  }
+
+  /**
+   * Returns an option list of possible delays. This list
+   * can be used in a select list
+   *
+   * Each element has a key which correspondents to the name of the class
+   * and the value to the description of the delay
+   *
+   * @return array
+   */
+  public static function getOptionList() {
+    $classes = self::getAllDelayClasses();
+    $options = array();
+    foreach($classes as $class) {
+      if ($class instanceof CRM_Civirules_Delay_Delay) {
+        $options[$class->getName()] = $class->getDescription();
+      }
+    }
+    asort($options);
+    return $options;
+  }
+
+}
\ No newline at end of file
diff --git a/CRM/Civirules/Delay/XDays.php b/CRM/Civirules/Delay/XDays.php
new file mode 100644
index 0000000..7e5b446
--- /dev/null
+++ b/CRM/Civirules/Delay/XDays.php
@@ -0,0 +1,34 @@
+<?php
+
+class CRM_Civirules_Delay_XDays extends CRM_Civirules_Delay_Delay {
+
+  protected $dayOffset;
+
+  public function delayTo(DateTime $date) {
+    $date->modify("+ ".$this->dayOffset." days");
+    return $date;
+  }
+
+  public function getDescription() {
+    return ts('Delay by a number of days');
+  }
+
+  public function getDelayExplanation() {
+    return ts('Delay action by %1 days', array(1 => $this->dayOffset));
+  }
+
+  public function addElements(CRM_Core_Form &$form) {
+    $form->add('text', 'xdays_dayOffset', ts('Days'));
+  }
+
+  public function validate($values, &$errors) {
+    if (empty($values['xdays_dayOffset']) || !is_numeric($values['xdays_dayOffset'])) {
+      $errors['xdays_dayOffset'] = ts('You need to provide a number of days');
+    }
+  }
+
+  public function setValues($values) {
+    $this->dayOffset = $values['xdays_dayOffset'];
+  }
+
+}
\ No newline at end of file
diff --git a/CRM/Civirules/Delay/XWeekDay.php b/CRM/Civirules/Delay/XWeekDay.php
new file mode 100644
index 0000000..82ba3c6
--- /dev/null
+++ b/CRM/Civirules/Delay/XWeekDay.php
@@ -0,0 +1,110 @@
+<?php
+
+class CRM_Civirules_Delay_XWeekDay extends CRM_Civirules_Delay_Delay {
+
+  protected $week_offset;
+
+  protected $day;
+
+  protected $time_hour = '9';
+
+  protected $time_minute = '00';
+
+  public function delayTo(DateTime $date) {
+    $d = clone $date;
+    $d->modify('-30 minutes');
+    $mod = $this->day.' of this week';
+    $date->modify($mod);
+    $date->setTime((int) $this->time_hour, (int) $this->time_minute);
+    if ($date <= $d) {
+      $date->modify('+1 week');
+      $date->modify($mod);
+    }
+    $weeknr = $date->format("W");
+    switch ($this->week_offset) {
+      case 'odd':
+        if(!($weeknr&1)) {
+          $date->modify('+1 week');
+          $date->modify($mod);
+        }
+        break;
+      case 'even':
+        if($weeknr&1) {
+          $date->modify('+1 week');
+          $date->modify($mod);
+        }
+        break;
+    }
+
+    return $date;
+  }
+
+  public function getDescription() {
+    return ts('Day of week');
+  }
+
+  public function getDelayExplanation() {
+    $offsets = $this->getWeekOffset();
+    return ts('Delay action to %1 of %2 at %3:%4',
+      array(
+        1 => ts($this->day),
+        2 => $offsets[$this->week_offset],
+        3 => $this->time_hour,
+        4 => $this->time_minute < 10 && strlen($this->time_minute) <= 1 ? '0'.$this->time_minute : $this->time_minute,
+      ));
+  }
+
+  public function addElements(CRM_Core_Form &$form) {
+    $form->add('select', 'XWeekDay_week_offset', ts('Offset'), $this->getWeekOffset());
+    $form->add('select', 'XWeekDay_day', ts('Days'), $this->getDays());
+    $form->add('text', 'XWeekDay_time_hour', ts('Time (hour)'));
+    $form->add('text', 'XWeekDay_time_minute', ts('Time (minute)'));
+  }
+
+  protected function getDays() {
+    return array(
+      'sunday' => ts('Sunday'),
+      'monday' => ts('Monday'),
+      'tuesday' => ts('Tuesday'),
+      'wednesday' => ts('Wednesday'),
+      'thursday' => ts('Thursday'),
+      'friday' => ts('Friday'),
+      'saturday' => ts('Saturday'),
+    );
+  }
+
+  protected function getWeekOffset() {
+    return array(
+      'every' => ts('Every week'),
+      'even' => ts('Even weeks'),
+      'odd' => ts('Odd weeks'),
+    );
+  }
+
+  public function validate($values, &$errors) {
+    if (empty($values['XWeekDay_time_hour']) || !is_numeric($values['XWeekDay_time_hour']) || $values['XWeekDay_time_hour'] < 0 || $values['XWeekDay_time_hour'] > 23) {
+      $errors['XWeekDay_time_hour'] = ts('You need to provide a number between 0 and 23');
+    }
+    if (empty($values['XWeekDay_time_minute']) || !is_numeric($values['XWeekDay_time_minute']) || $values['XWeekDay_time_minute'] < 0 || $values['XWeekDay_time_minute'] > 59) {
+      $errors['XWeekDay_time_minute'] = ts('You need to provide a number between 0 and 59');
+    }
+  }
+
+  public function setValues($values) {
+    $this->week_offset = $values['XWeekDay_week_offset'];
+    $this->day = $values['XWeekDay_day'];
+    $this->time_hour = $values['XWeekDay_time_hour'];
+    $this->time_minute = $values['XWeekDay_time_minute'];
+  }
+
+  /**
+   * Set default values
+   *
+   * @param $values
+   */
+  public function setDefaultValues(&$values) {
+    $values['XWeekDay_time_hour'] = '9';
+    $values['XWeekDay_time_minute'] = '00';
+  }
+
+}
\ No newline at end of file
diff --git a/CRM/Civirules/Delay/XWeekDayOfMonth.php b/CRM/Civirules/Delay/XWeekDayOfMonth.php
new file mode 100644
index 0000000..024b624
--- /dev/null
+++ b/CRM/Civirules/Delay/XWeekDayOfMonth.php
@@ -0,0 +1,97 @@
+<?php
+
+class CRM_Civirules_Delay_XWeekDayOfMonth extends CRM_Civirules_Delay_Delay {
+
+  protected $week_offset;
+
+  protected $day;
+
+  protected $time_hour = '9';
+
+  protected $time_minute = '00';
+
+  public function delayTo(DateTime $date) {
+    $d = clone $date;
+    $d->modify('-30 minutes');
+    $mod = $this->week_offset .' '.$this->day.' of this month';
+    $date->modify($mod);
+    $date->setTime((int) $this->time_hour, (int) $this->time_minute);
+    if ($date <= $d) {
+      $date->modify('first day of next month');
+      $date->modify($mod);
+    }
+
+    return $date;
+  }
+
+  public function getDescription() {
+    return ts('Nth weekday of month');
+  }
+
+  public function getDelayExplanation() {
+    return ts('Delay action to %1 %2 at %3:%4',
+      array(
+        1 => ts($this->week_offset),
+        2 => ts($this->day),
+        3 => $this->time_hour,
+        4 => $this->time_minute < 10 && strlen($this->time_minute) <= 1 ? '0'.$this->time_minute : $this->time_minute,
+      ));
+  }
+
+  public function addElements(CRM_Core_Form &$form) {
+    $form->add('select', 'XWeekDayOfMonth_week_offset', ts('Offset'), $this->getWeekOffset());
+    $form->add('select', 'XWeekDayOfMonth_day', ts('Days'), $this->getDays());
+    $form->add('text', 'XWeekDayOfMonth_time_hour', ts('Time (hour)'));
+    $form->add('text', 'XWeekDayOfMonth_time_minute', ts('Time (minute)'));
+  }
+
+  protected function getDays() {
+    return array(
+      'sunday' => ts('Sunday'),
+      'monday' => ts('Monday'),
+      'tuesday' => ts('Tuesday'),
+      'wednesday' => ts('Wednesday'),
+      'thursday' => ts('Thursday'),
+      'friday' => ts('Friday'),
+      'saturday' => ts('Saturday'),
+    );
+  }
+
+  protected function getWeekOffset() {
+    return array(
+      'first' => ts('First'),
+      'second' => ts('Second'),
+      'third' => ts('Third'),
+      'fourth' => ts('Fourth'),
+      'fifth' => ts('Fifth'),
+      'last' => ts('Last'),
+    );
+  }
+
+  public function validate($values, &$errors) {
+    if (empty($values['XWeekDayOfMonth_time_hour']) || !is_numeric($values['XWeekDayOfMonth_time_hour']) || $values['XWeekDayOfMonth_time_hour'] < 0 || $values['XWeekDayOfMonth_time_hour'] > 23) {
+      $errors['XWeekDayOfMonth_time_hour'] = ts('You need to provide a number between 0 and 23');
+    }
+    if (empty($values['XWeekDayOfMonth_time_minute']) || !is_numeric($values['XWeekDayOfMonth_time_minute']) || $values['XWeekDayOfMonth_time_minute'] < 0 || $values['XWeekDayOfMonth_time_minute'] > 59) {
+      $errors['XWeekDayOfMonth_time_minute'] = ts('You need to provide a number between 0 and 59');
+    }
+  }
+
+  public function setValues($values) {
+    $this->week_offset = $values['XWeekDayOfMonth_week_offset'];
+    $this->day = $values['XWeekDayOfMonth_day'];
+    $this->time_hour = $values['XWeekDayOfMonth_time_hour'];
+    $this->time_minute = $values['XWeekDayOfMonth_time_minute'];
+  }
+
+  /**
+   * Set default values
+   *
+   * @param $values
+   */
+  public function setDefaultValues(&$values) {
+    $values['XWeekDayOfMonth_time_hour'] = '9';
+    $values['XWeekDayOfMonth_time_minute'] = '00';
+  }
+
+}
\ No newline at end of file
diff --git a/CRM/Civirules/Engine.php b/CRM/Civirules/Engine.php
index 6f58330..828a851 100644
--- a/CRM/Civirules/Engine.php
+++ b/CRM/Civirules/Engine.php
@@ -8,6 +8,8 @@
 
 class CRM_Civirules_Engine {
 
+  const QUEUE_NAME = 'org.civicoop.civirules.action';
+
   /**
    * Trigger a rule.
    *
@@ -60,7 +62,124 @@ class CRM_Civirules_Engine {
     }
 
     $object->setRuleActionData($ruleAction);
-    $object->processAction($eventData);
+
+    //determine if the action should be executed with a delay
+    $delay = self::getActionDelay($ruleAction);
+    if ($delay instanceof DateTime) {
+      self::delayAction($delay, $object, $eventData);
+    } else {
+      //there is no delay so process action immediatly
+      $object->processAction($eventData);
+    }
+  }
+
+  /**
+   * Process delayed actions
+   *
+   * @param int $maxRunTime
+   * @return array
+   */
+  public static function processDelayedActions($maxRunTime=30) {
+    $queue = CRM_Queue_Service::singleton()->create(array(
+      'type' => 'Civirules',
+      'name' => self::QUEUE_NAME,
+      'reset' => false, //do not flush queue upon creation
+    ));
+
+    $returnValues = array();
+
+    //retrieve the queue
+    $runner = new CRM_Queue_Runner(array(
+      'title' => ts('Process delayed civirules actions'), //title fo the queue
+      'queue' => $queue, //the queue object
+      'errorMode'=> CRM_Queue_Runner::ERROR_CONTINUE, //continue on error otherwise the queue will hang
+    ));
+
+    $stopTime = time() + $maxRunTime; //stop executing next item after 30 seconds
+    while((time() < $stopTime)) {
+      $result = $runner->runNext(false);
+      $returnValues[] = $result;
+
+      if (!$result['is_continue']) {
+        break;
+      }
+    }
+
+    return $returnValues;
+  }
+
+  /**
+   * Executes a delayed action
+   *
+   * @param \CRM_Queue_TaskContext $ctx
+   * @param \CRM_Civirules_Action $action
+   * @param \CRM_Civirules_EventData_EventData $eventData
+   * @return bool
+   */
+  public static function executeDelayedAction(CRM_Queue_TaskContext $ctx, CRM_Civirules_Action $action, CRM_Civirules_EventData_EventData $eventData) {
+    $action->processAction($eventData);
+    return true;
+  }
+
+  /**
+   * Save an action into a queue for delayed processing
+   *
+   * @param \DateTime $delayTo
+   * @param \CRM_Civirules_Action $action
+   * @param \CRM_Civirules_EventData_EventData $eventData
+   */
+  protected static function delayAction(DateTime $delayTo, CRM_Civirules_Action $action, CRM_Civirules_EventData_EventData $eventData) {
+    $queue = CRM_Queue_Service::singleton()->create(array(
+      'type' => 'Civirules',
+      'name' => self::QUEUE_NAME,
+      'reset' => false, //do not flush queue upon creation
+    ));
+
+    //create a task with the action and eventData as parameters
+    $task = new CRM_Queue_Task(
+      array('CRM_Civirules_Engine', 'executeDelayedAction'), //call back method
+      array($action, $eventData) //parameters
+    );
+
+    //save the task with a delay
+    $dao              = new CRM_Queue_DAO_QueueItem();
+    $dao->queue_name  = $queue->getName();
+    $dao->submit_time = CRM_Utils_Time::getTime('YmdHis');
+    $dao->data        = serialize($task);
+    $dao->weight      = 0; //weight, normal priority
+    $dao->release_time = $delayTo->format('YmdHis');
+    $dao->save();
+  }
+
+  /**
+   * Returns false when action could not be delayed or return a DateTime
+   * This DateTime object holds the date and time till when the action should be delayed
+   *
+   * The delay is calculated by a seperate delay class. See CRM_Civirules_DelayDelay
+   *
+   * @param $ruleAction
+   * @return bool|\DateTime
+   */
+  protected static function getActionDelay($ruleAction) {
+    //if the delay is empty the
+    if (empty($ruleAction['delay'])) {
+      return false;
+    }
+
+    $delayClass = unserialize(($ruleAction['delay']));
+    if (! ($delayClass instanceof CRM_Civirules_Delay_Delay)) {
+      return false;
+    }
+
+    $delayedTo = new DateTime();
+    $now = new DateTime();
+    $delayedTo = $delayClass->delayTo($delayedTo);
+
+    if ($now >= $delayedTo) {
+      return false;
+    }
+
+    return $delayedTo;
   }
 
   /**
diff --git a/CRM/Civirules/Form/Rule.php b/CRM/Civirules/Form/Rule.php
index 38b251e..6ea4012 100755
--- a/CRM/Civirules/Form/Rule.php
+++ b/CRM/Civirules/Form/Rule.php
@@ -277,6 +277,12 @@ class CRM_Civirules_Form_Rule extends CRM_Core_Form {
       $actionClass = CRM_Civirules_BAO_Action::getActionObjectById($ruleAction['action_id']);
       $actionClass->setRuleActionData($ruleAction);
       $ruleActions[$ruleActionId]['formattedConditionParams'] = $actionClass->userFriendlyConditionParams();
+
+      $ruleActions[$ruleActionId]['formattedDelay'] = '';
+      if (!empty($ruleAction['delay'])) {
+        $delayClass = unserialize($ruleAction['delay']);
+        $ruleActions[$ruleActionId]['formattedDelay'] = $delayClass->getDelayExplanation();
+      }
     }
     return $ruleActions;
   }
diff --git a/CRM/Civirules/Form/RuleAction.php b/CRM/Civirules/Form/RuleAction.php
index e6ab9fc..fef324c 100644
--- a/CRM/Civirules/Form/RuleAction.php
+++ b/CRM/Civirules/Form/RuleAction.php
@@ -50,8 +50,16 @@ class CRM_Civirules_Form_RuleAction extends CRM_Core_Form {
 
     $saveParams = array(
       'rule_id' => $this->_submitValues['rule_id'],
-      'action_id' => $this->_submitValues['rule_action_select']
+      'action_id' => $this->_submitValues['rule_action_select'],
+      'delay' => 'null',
     );
+
+    if (!empty($this->_submitValues['delay_select'])) {
+      $delayClass = CRM_Civirules_Delay_Factory::getDelayClassByName($this->_submitValues['delay_select']);
+      $delayClass->setValues($this->_submitValues);
+      $saveParams['delay'] = serialize($delayClass);
+    }
+
     $ruleAction = CRM_Civirules_BAO_RuleAction::add($saveParams);
 
     $session = CRM_Core_Session::singleton();
@@ -78,6 +86,13 @@ class CRM_Civirules_Form_RuleAction extends CRM_Core_Form {
     asort($actionList);
     $this->add('select', 'rule_action_select', ts('Select Action'), $actionList);
 
+    $delayList = array(' - No Delay - ') + CRM_Civirules_Delay_Factory::getOptionList();
+    $this->add('select', 'delay_select', ts('Delay action to'), $delayList);
+    foreach(CRM_Civirules_Delay_Factory::getAllDelayClasses() as $delay_class) {
+      $delay_class->addElements($this);
+    }
+    $this->assign('delayClasses', CRM_Civirules_Delay_Factory::getAllDelayClasses());
+
     $this->addButtons(array(
       array('type' => 'next', 'name' => ts('Save'), 'isDefault' => TRUE,),
       array('type' => 'cancel', 'name' => ts('Cancel'))));
@@ -85,6 +100,11 @@ class CRM_Civirules_Form_RuleAction extends CRM_Core_Form {
 
   public function setDefaultValues() {
     $defaults['rule_id'] = $this->ruleId;
+
+    foreach(CRM_Civirules_Delay_Factory::getAllDelayClasses() as $delay_class) {
+      $delay_class->setDefaultValues($defaults);
+    }
+
     return $defaults;
   }
 
@@ -117,10 +137,19 @@ class CRM_Civirules_Form_RuleAction extends CRM_Core_Form {
    * @static
    */
   static function validateRuleAction($fields) {
+    $errors = array();
     if (isset($fields['rule_action_select']) && empty($fields['rule_action_select'])) {
       $errors['rule_action_select'] = ts('Action has to be selected, press CANCEL if you do not want to add an action');
+    }
+    if (!empty($fields['delay_select'])) {
+      $delayClass = CRM_Civirules_Delay_Factory::getDelayClassByName($fields['delay_select']);
+      $delayClass->validate($fields, $errors);
+    }
+
+    if (count($errors)) {
       return $errors;
     }
+
     return TRUE;
   }
 }
diff --git a/CRM/Queue/Queue/Civirules.php b/CRM/Queue/Queue/Civirules.php
new file mode 100644
index 0000000..8f2f0a4
--- /dev/null
+++ b/CRM/Queue/Queue/Civirules.php
@@ -0,0 +1,21 @@
+<?php
+
+class CRM_Queue_Queue_Civirules extends CRM_Queue_Queue_Sql {
+
+  /**
+   * Determine number of items remaining in the queue
+   *
+   * @return int
+   */
+  function numberOfItems() {
+    return CRM_Core_DAO::singleValueQuery("
+      SELECT count(*)
+      FROM civicrm_queue_item
+      WHERE queue_name = %1
+      and (release_time is null OR release_time <= curdate())
+    ", array(
+      1 => array($this->getName(), 'String'),
+    ));
+  }
+
+}
\ No newline at end of file
diff --git a/api/v3/CiviRuleAction/Process.mgd.php b/api/v3/CiviRuleAction/Process.mgd.php
new file mode 100644
index 0000000..8504e47
--- /dev/null
+++ b/api/v3/CiviRuleAction/Process.mgd.php
@@ -0,0 +1,20 @@
+<?php
+
+return array (
+  0 =>
+    array (
+      'name' => 'Cron:CiviRuleAction.Process',
+      'entity' => 'Job',
+      'params' =>
+        array (
+          'version' => 3,
+          'name' => 'Process delayed civirule actions',
+          'description' => '',
+          'run_frequency' => 'Always',
+          'api_entity' => 'CiviRuleAction',
+          'api_action' => 'Process',
+          'parameters' => '',
+          'is_active' => '1',
+        ),
+    ),
+);
\ No newline at end of file
diff --git a/api/v3/CiviRuleAction/Process.php b/api/v3/CiviRuleAction/Process.php
new file mode 100644
index 0000000..7ab8302
--- /dev/null
+++ b/api/v3/CiviRuleAction/Process.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * CiviRuleAction.process API
+ *
+ * Process delayed actions
+ *
+ * @param array $params
+ * @return array API result descriptor
+ * @see civicrm_api3_create_success
+ * @see civicrm_api3_create_error
+ * @throws API_Exception
+ */
+function civicrm_api3_civi_rule_action_process($params) {
+  $returnValues = CRM_Civirules_Engine::processDelayedActions(60);
+
+  // Spec: civicrm_api3_create_success($values = 1, $params = array(), $entity = NULL, $action = NULL)
+  return civicrm_api3_create_success($returnValues, $params, 'CiviRuleAction', 'Process');
+}
\ No newline at end of file
diff --git a/sql/createCiviruleRuleAction.sql b/sql/createCiviruleRuleAction.sql
index 85965c0..de8d397 100755
--- a/sql/createCiviruleRuleAction.sql
+++ b/sql/createCiviruleRuleAction.sql
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS civirule_rule_action (
   rule_id INT UNSIGNED NULL,
   action_id INT UNSIGNED NULL,
   action_params TEXT NULL,
+  delay TEXT NULL,
   is_active TINYINT NULL DEFAULT 1,
   PRIMARY KEY (id),
   UNIQUE INDEX id_UNIQUE (id ASC),
diff --git a/templates/CRM/Civirules/Delay/XDays.tpl b/templates/CRM/Civirules/Delay/XDays.tpl
new file mode 100644
index 0000000..d50180d
--- /dev/null
+++ b/templates/CRM/Civirules/Delay/XDays.tpl
@@ -0,0 +1,3 @@
+<div class="label">{$form.xdays_dayOffset.label}</div>
+<div class="content">{$form.xdays_dayOffset.html}</div>
+<div class="clear"></div>
\ No newline at end of file
diff --git a/templates/CRM/Civirules/Delay/XWeekDay.tpl b/templates/CRM/Civirules/Delay/XWeekDay.tpl
new file mode 100644
index 0000000..3101eec
--- /dev/null
+++ b/templates/CRM/Civirules/Delay/XWeekDay.tpl
@@ -0,0 +1,6 @@
+<div class="label"></div>
+<div class="content">{$form.XWeekDay_week_offset.html} {ts}on{/ts} {$form.XWeekDay_day.html} </div>
+<div class="clear"></div>
+<div class="label">{ts}After{/ts}</div>
+<div class="content">{$form.XWeekDay_time_hour.html} : {$form.XWeekDay_time_minute.html}</div>
+<div class="clear"></div>
\ No newline at end of file
diff --git a/templates/CRM/Civirules/Delay/XWeekDayOfMonth.tpl b/templates/CRM/Civirules/Delay/XWeekDayOfMonth.tpl
new file mode 100644
index 0000000..e507f50
--- /dev/null
+++ b/templates/CRM/Civirules/Delay/XWeekDayOfMonth.tpl
@@ -0,0 +1,6 @@
+<div class="label"></div>
+<div class="content">{$form.XWeekDayOfMonth_week_offset.html} {$form.XWeekDayOfMonth_day.html}</div>
+<div class="clear"></div>
+<div class="label">{ts}After{/ts}</div>
+<div class="content">{$form.XWeekDayOfMonth_time_hour.html} : {$form.XWeekDayOfMonth_time_minute.html}</div>
+<div class="clear"></div>
\ No newline at end of file
diff --git a/templates/CRM/Civirules/Form/RuleAction.tpl b/templates/CRM/Civirules/Form/RuleAction.tpl
index 829a83a..720abcc 100644
--- a/templates/CRM/Civirules/Form/RuleAction.tpl
+++ b/templates/CRM/Civirules/Form/RuleAction.tpl
@@ -1,12 +1,46 @@
 {* block for rule condition data *}
 <h3>{$ruleActionHeader}</h3>
-<div class="crm-block crm-form-block crm-civirule-rule_condition-block">
+<div class="crm-block crm-form-block crm-civirule-rule_action-block">
   <div class="crm-section">
     <div class="label">{$form.rule_action_select.label}</div>
     <div class="content">{$form.rule_action_select.html}</div>
     <div class="clear"></div>
   </div>
 </div>
+<h3>{ts}Delay action{/ts}</h3>
+<div class="crm-block crm-form-block crm-civirule-rule_action_delay-block">
+    <div class="crm-section">
+        <div class="label">{$form.delay_select.label}</div>
+        <div class="content">{$form.delay_select.html}</div>
+        <div class="clear"></div>
+    </div>
+    {foreach from=$delayClasses item=delayClass}
+        <div class="crm-section crm-delay-class" id="{$delayClass->getName()}">
+            <div class="label"></div>
+            <div class="content"><strong>{$delayClass->getDescription()}</strong></div>
+            <div class="clear"></div>
+            {include file=$delayClass->getTemplateFilename()}
+        </div>
+    {/foreach}
+</div>
 <div class="crm-submit-buttons">
   {include file="CRM/common/formButtons.tpl" location="bottom"}
 </div>
+
+{literal}
+<script type="text/javascript">
+cj(function() {
+    cj('select#delay_select').change(triggerDelayChange);
+
+    triggerDelayChange();
+});
+
+function triggerDelayChange() {
+    cj('.crm-delay-class').css('display', 'none');
+    var val = cj('#delay_select').val();
+    if (val) {
+        cj('#'+val).css('display', 'block');
+    }
+}
+</script>
+{/literal}
diff --git a/templates/CRM/Civirules/Form/RuleBlocks/ActionBlock.tpl b/templates/CRM/Civirules/Form/RuleBlocks/ActionBlock.tpl
index 57cea17..b5ccd9b 100755
--- a/templates/CRM/Civirules/Form/RuleBlocks/ActionBlock.tpl
+++ b/templates/CRM/Civirules/Form/RuleBlocks/ActionBlock.tpl
@@ -9,6 +9,7 @@
           <tr>
             <th>{ts}Name{/ts}</th>
             <th>{ts}Extra parameters{/ts}</th>
+            <th class="nosort">&nbsp;</th>
             <th id="nosort">&nbsp;</th>
           </tr>
         </thead>
@@ -24,6 +25,9 @@
               {else}
                 <td>&nbsp;</td>
               {/if}
+              <td>
+                  {$ruleAction.formattedDelay}
+              </td>
               <td>
                 <span>
                   {foreach from=$ruleAction.actions item=actionLink}
diff --git a/xml/Menu/civirules.xml b/xml/Menu/civirules.xml
index 74601b0..d7941b9 100755
--- a/xml/Menu/civirules.xml
+++ b/xml/Menu/civirules.xml
@@ -44,7 +44,7 @@
   </item>
   <item>
       <path>civicrm/civirule/form/condition/contributionstatus</path>
-      <page_callback>CRM_CivirulesConditions_Form_ContributionStatus</page_callback>
+      <page_callback>CRM_CivirulesConditions_Form_Contribution_Status</page_callback>
       <title>contribution status</title>
       <access_arguments>access CiviCRM</access_arguments>
   </item>
-- 
GitLab