From c0fc0432bb379ba6e7eb65b569f24efd91edd2b3 Mon Sep 17 00:00:00 2001 From: Jon Goldberg Date: Sat, 1 Aug 2020 17:25:23 -0400 Subject: [PATCH] move statusBounce out of BAO layer; don't allow self-service when disabled; support cancel deadlines > 24 hours. --- CRM/Event/BAO/Participant.php | 44 ++++++++-------- CRM/Event/Form/SelfSvcTransfer.php | 5 +- CRM/Event/Form/SelfSvcUpdate.php | 5 +- .../phpunit/CRM/Event/BAO/ParticipantTest.php | 50 ++++++++++++------- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/CRM/Event/BAO/Participant.php b/CRM/Event/BAO/Participant.php index aa0aaf6170..1f19c32f74 100644 --- a/CRM/Event/BAO/Participant.php +++ b/CRM/Event/BAO/Participant.php @@ -1877,19 +1877,15 @@ WHERE civicrm_participant.contact_id = {$contactID} AND * Evaluate whether a participant record is eligible for self-service transfer/cancellation. If so, * return additional participant/event details. * - * TODO: BAO-level functions shouldn't set a redirect, and it should be possible to return "false" to the - * calling function. The next refactor will add a fourth param $errors, which can be passed by reference - * from the calling function. Instead of redirecting, we will return the error. - * TODO: This function should always return FALSE when self-service has been disabled on an event. - * TODO: This function fails when the "hours until self-service" is greater than 24 or less than zero. + * TODO: This function fails when the "hours until self-service" is less than zero. * @param int $participantId * @param string $url * @param bool $isBackOffice */ - public static function getSelfServiceEligibility($participantId, $url, $isBackOffice) { + public static function getSelfServiceEligibility(int $participantId, string $url, bool $isBackOffice) : array { $optionGroupId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', 'participant_role', 'id', 'name'); $query = " - SELECT cpst.name as status, cov.name as role, cp.fee_level, cp.fee_amount, cp.register_date, cp.status_id, ce.start_date, ce.title, cp.event_id + SELECT cpst.name as status, cov.name as role, cp.fee_level, cp.fee_amount, cp.register_date, cp.status_id, ce.start_date, ce.title, cp.event_id, ce.allow_selfcancelxfer FROM civicrm_participant cp LEFT JOIN civicrm_participant_status_type cpst ON cpst.id = cp.status_id LEFT JOIN civicrm_option_value cov ON cov.value = cp.role_id and cov.option_group_id = {$optionGroupId} @@ -1897,40 +1893,44 @@ WHERE civicrm_participant.contact_id = {$contactID} AND WHERE cp.id = {$participantId}"; $dao = CRM_Core_DAO::executeQuery($query); while ($dao->fetch()) { + $details['eligible'] = TRUE; $details['status'] = $dao->status; $details['role'] = $dao->role; $details['fee_level'] = trim($dao->fee_level, CRM_Core_DAO::VALUE_SEPARATOR); $details['fee_amount'] = $dao->fee_amount; $details['register_date'] = $dao->register_date; $details['event_start_date'] = $dao->start_date; + $details['allow_selfcancelxfer'] = $dao->allow_selfcancelxfer; $eventTitle = $dao->title; $eventId = $dao->event_id; } + if (!$details['allow_selfcancelxfer']) { + $details['eligible'] = FALSE; + $details['ineligible_message'] = ts('This event registration can not be transferred or cancelled. Contact the event organizer if you have questions.'); + return $details; + } //verify participant status is still Registered if ($details['status'] != 'Registered') { - $status = "You cannot transfer or cancel your registration for " . $eventTitle . ' as you are not currently registered for this event.'; - CRM_Core_Session::setStatus($status, ts('Sorry'), 'alert'); - CRM_Utils_System::redirect($url); + $details['eligible'] = FALSE; + $details['ineligible_message'] = "You cannot transfer or cancel your registration for " . $eventTitle . ' as you are not currently registered for this event.'; + return $details; } + // Determine if it's too late to self-service cancel/transfer. $query = "select start_date as start, selfcancelxfer_time as time from civicrm_event where id = " . $eventId; $dao = CRM_Core_DAO::executeQuery($query); while ($dao->fetch()) { $time_limit = $dao->time; $start_date = $dao->start; } - $start_time = new Datetime($start_date); $timenow = new Datetime(); - if (!$isBackOffice && !empty($start_time) && $start_time < $timenow) { - $status = ts('Registration for this event cannot be cancelled or transferred once the event has begun. Contact the event organizer if you have questions.'); - CRM_Core_Error::statusBounce($status, $url, ts('Sorry')); - } - if (!$isBackOffice && !empty($time_limit) && $time_limit > 0) { - $interval = $timenow->diff($start_time); - $days = $interval->format('%d'); - $hours = $interval->format('%h'); - if ($hours <= $time_limit && $days < 1) { - $status = ts("Registration for this event cannot be cancelled or transferred less than %1 hours prior to the event's start time. Contact the event organizer if you have questions.", [1 => $time_limit]); - CRM_Core_Error::statusBounce($status, $url, ts('Sorry')); + if (!$isBackOffice && !empty($time_limit)) { + $cancelHours = abs($time_limit); + $cancelInterval = new DateInterval("PT${cancelHours}H"); + $cancelInterval->invert = $time_limit < 0 ? 1 : 0; + $cancelDeadline = (new Datetime($start_date))->sub($cancelInterval); + if ($timenow > $cancelDeadline) { + $details['eligible'] = FALSE; + $details['ineligible_message'] = ts("Registration for this event cannot be cancelled or transferred less than %1 hours prior to the event's start time. Contact the event organizer if you have questions.", [1 => $time_limit]); } } return $details; diff --git a/CRM/Event/Form/SelfSvcTransfer.php b/CRM/Event/Form/SelfSvcTransfer.php index 5d72f2aaa3..1d5ac36d72 100644 --- a/CRM/Event/Form/SelfSvcTransfer.php +++ b/CRM/Event/Form/SelfSvcTransfer.php @@ -137,7 +137,7 @@ class CRM_Event_Form_SelfSvcTransfer extends CRM_Core_Form { $this->_userContext = $session->readUserContext(); $this->_from_participant_id = CRM_Utils_Request::retrieve('pid', 'Positive', $this, FALSE, NULL, 'REQUEST'); $this->_userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this, FALSE, NULL, 'REQUEST'); - $this->isBackoffice = CRM_Utils_Request::retrieve('is_backoffice', 'String', $this, FALSE, NULL, 'REQUEST'); + $this->isBackoffice = CRM_Utils_Request::retrieve('is_backoffice', 'String', $this, FALSE, NULL, 'REQUEST') ?? FALSE; $params = ['id' => $this->_from_participant_id]; $participant = $values = []; $this->_participant = CRM_Event_BAO_Participant::getValues($params, $values, $participant); @@ -165,6 +165,9 @@ class CRM_Event_Form_SelfSvcTransfer extends CRM_Core_Form { $details = CRM_Event_BAO_Participant::participantDetails($this->_from_participant_id); $selfServiceDetails = CRM_Event_BAO_Participant::getSelfServiceEligibility($this->_from_participant_id, $url, $this->isBackoffice); + if (!$selfServiceDetails['eligible']) { + CRM_Core_Error::statusBounce($selfServiceDetails['ineligible_message'], $url, ts('Sorry')); + } $details = array_merge($details, $selfServiceDetails); $this->assign('details', $details); //This participant row will be cancelled. Get line item(s) to cancel diff --git a/CRM/Event/Form/SelfSvcUpdate.php b/CRM/Event/Form/SelfSvcUpdate.php index 33b9f7f3f8..a87d7aff27 100644 --- a/CRM/Event/Form/SelfSvcUpdate.php +++ b/CRM/Event/Form/SelfSvcUpdate.php @@ -110,7 +110,7 @@ class CRM_Event_Form_SelfSvcUpdate extends CRM_Core_Form { $participant = $values = []; $this->_participant_id = CRM_Utils_Request::retrieve('pid', 'Positive', $this, FALSE, NULL, 'REQUEST'); $this->_userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this, FALSE, NULL, 'REQUEST'); - $this->isBackoffice = CRM_Utils_Request::retrieve('is_backoffice', 'String', $this, FALSE, NULL, 'REQUEST'); + $this->isBackoffice = CRM_Utils_Request::retrieve('is_backoffice', 'String', $this, FALSE, FALSE, 'REQUEST') ?? FALSE; $params = ['id' => $this->_participant_id]; $this->_participant = CRM_Event_BAO_Participant::getValues($params, $values, $participant); $this->_part_values = $values[$this->_participant_id]; @@ -140,6 +140,9 @@ class CRM_Event_Form_SelfSvcUpdate extends CRM_Core_Form { $contributionId = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_ParticipantPayment', $this->_participant_id, 'contribution_id', 'participant_id'); $this->assign('contributionId', $contributionId); $selfServiceDetails = CRM_Event_BAO_Participant::getSelfServiceEligibility($this->_participant_id, $url, $this->isBackoffice); + if (!$selfServiceDetails['eligible']) { + CRM_Core_Error::statusBounce($selfServiceDetails['ineligible_message'], $url, ts('Sorry')); + } $details = array_merge($details, $selfServiceDetails); $this->assign('details', $details); $this->selfsvcupdateUrl = CRM_Utils_System::url('civicrm/event/selfsvcupdate', "reset=1&id={$this->_participant_id}&id=0"); diff --git a/tests/phpunit/CRM/Event/BAO/ParticipantTest.php b/tests/phpunit/CRM/Event/BAO/ParticipantTest.php index 0a3cba0fd9..eb8dc57008 100644 --- a/tests/phpunit/CRM/Event/BAO/ParticipantTest.php +++ b/tests/phpunit/CRM/Event/BAO/ParticipantTest.php @@ -434,27 +434,14 @@ class CRM_Event_BAO_ParticipantTest extends CiviUnitTestCase { 'start_date' => $startDate, ]); $url = CRM_Utils_System::url('civicrm/event/info', "reset=1&id={$this->_eventId}"); - // If we return without an error, then success. But we don't always expect success. - try { - CRM_Event_BAO_Participant::getSelfServiceEligibility($participantId, $url, $isBackOffice); - } - catch (\Exception $e) { - if ($successExpected === FALSE) { - return; - } - else { - $this->fail(); - } - } - if ($successExpected === FALSE) { - $this->fail(); - } + $details = CRM_Event_BAO_Participant::getSelfServiceEligibility($participantId, $url, $isBackOffice); + $this->assertEquals($details['eligible'], $successExpected); } public function selfServiceScenarios() { // Standard pass scenario $scenarios[] = [ - 'selfSvcEnabled' => TRUE, + 'selfSvcEnabled' => 1, 'selfSvcHours' => 12, 'hoursToEvent' => 16, 'participantStatusId' => 1, @@ -463,7 +450,7 @@ class CRM_Event_BAO_ParticipantTest extends CiviUnitTestCase { ]; // Too late to self-service $scenarios[] = [ - 'selfSvcEnabled' => TRUE, + 'selfSvcEnabled' => 1, 'selfSvcHours' => 12, 'hoursToEvent' => 8, 'participantStatusId' => 1, @@ -472,13 +459,40 @@ class CRM_Event_BAO_ParticipantTest extends CiviUnitTestCase { ]; // Participant status is other than "Registered". $scenarios[] = [ - 'selfSvcEnabled' => TRUE, + 'selfSvcEnabled' => 1, 'selfSvcHours' => 12, 'hoursToEvent' => 16, 'participantStatusId' => 2, 'isBackOffice' => FALSE, 'successExpected' => FALSE, ]; + // Event doesn't allow self-service + $scenarios[] = [ + 'selfSvcEnabled' => 0, + 'selfSvcHours' => 12, + 'hoursToEvent' => 16, + 'participantStatusId' => 1, + 'isBackOffice' => FALSE, + 'successExpected' => FALSE, + ]; + // Cancellation deadline is > 24 hours, still ok to cancel + $scenarios[] = [ + 'selfSvcEnabled' => 1, + 'selfSvcHours' => 36, + 'hoursToEvent' => 46, + 'participantStatusId' => 1, + 'isBackOffice' => FALSE, + 'successExpected' => TRUE, + ]; + // Cancellation deadline is > 24 hours, too late to cancel + $scenarios[] = [ + 'selfSvcEnabled' => 1, + 'selfSvcHours' => 36, + 'hoursToEvent' => 25, + 'participantStatusId' => 1, + 'isBackOffice' => FALSE, + 'successExpected' => FALSE, + ]; return $scenarios; } -- 2.25.1