Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAE-318: Use queue to process memberships #348

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
115 changes: 98 additions & 17 deletions CRM/MembershipExtras/Job/OfflineAutoRenewal.php
Original file line number Diff line number Diff line change
@@ -1,39 +1,120 @@
<?php

use CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_MultipleInstalmentPlan as OfflineAutoRenewalMultipleInstalmentPlanQueueBuilder;
use CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_SingleInstalmentPlan as OfflineAutoRenewalSingleInstalmentPlanQueueBuilder;
use CRM_MembershipExtras_Queue_Task_PostRunAll as PostRunAllTask;
use CRM_MembershipExtras_Queue_OfflineAutoRenewal as OfflineAutoRenewalQueue;

class CRM_MembershipExtras_Job_OfflineAutoRenewal {

private $queue;

/**
* @var int
*/
private $numberOfQueueItems;

public function __construct() {
$this->queue = OfflineAutoRenewalQueue::getQueue();
$this->numberOfQueueItems = (int) $this->queue->numberOfItems();
}

/**
* Starts the scheduled job for renewing offline
* auto-renewal memberships.
*
* @return True
*
* @throws \CRM_Core_Exception
*/
public function run() {
$exceptions = [];
$this->addTasksToQueue();
$this->runQueue();

try {
$multipleInstalmentRenewal = new CRM_MembershipExtras_Job_OfflineAutoRenewal_MultipleInstalmentPlan();
$multipleInstalmentRenewal->run();
}
catch (CRM_Core_Exception $e) {
$exceptions[] = $e->getMessage();
return TRUE;
}

private function addTasksToQueue() {
if ($this->numberOfQueueItems > 0) {
return;
}

try {
$singleInstalmentRenewal = new CRM_MembershipExtras_Job_OfflineAutoRenewal_SingleInstalmentPlan();
$singleInstalmentRenewal->run();
$queueBuilders = [
new OfflineAutoRenewalMultipleInstalmentPlanQueueBuilder($this->queue),
new OfflineAutoRenewalSingleInstalmentPlanQueueBuilder($this->queue),
];
foreach ($queueBuilders as $queueBuilder) {
$queueBuilder->run();
}
catch (CRM_Core_Exception $e) {
$exceptions[] = $e->getMessage();

$this->numberOfQueueItems = (int) $this->queue->numberOfItems();
}

private function runQueue() {
if ($this->numberOfQueueItems === 0) {
return;
}

if (count($exceptions)) {
throw new CRM_Core_Exception("Errors found on auto-renewals: " . implode("\n", $exceptions));
$this->addPostRunAllTask();

$runner = new CRM_Queue_Runner([
'title' => ts('Processing membership renewals, this may take a while depending on how many records are processed ..'),
'queue' => $this->queue,
'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE,
'onEnd' => array('CRM_MembershipExtras_Job_OfflineAutoRenewal', 'onEnd'),
'onEndUrl' => CRM_Utils_System::url('civicrm/admin/job', ['reset' => 1]),
]);

// Only use `runAllViaWeb` if the admin executed the job from CiviCRM UI.
$currentPath = CRM_Utils_System::currentPath();
if ($currentPath === 'civicrm/admin/job') {
$runner->runAllViaWeb();
}
else {
$runner->runAll();
}
}

return TRUE;
/**
* @param \CRM_Queue_TaskContext $ctx
*
* @throws \CiviCRM_API3_Exception
*/
public static function onEnd(CRM_Queue_TaskContext $ctx) {
$job = civicrm_api3('Job', 'getSingle', [
'name' => 'Renew offline auto-renewal memberships',
]);

$result = civicrm_api3('JobLog', 'create', [
'domain_id' => $job['domain_id'],
'job_id' => $job['id'],
'name' => $job['name'],
'command' => ts("Entity:") . " " . $job['api_entity'] . " " . ts("Action:") . " " . $job['api_action'],
'description' => 'Finished execution of Renew offline auto-renewal memberships with result: Success',
'data' => "
Full message:
Finished execution of Renew offline auto-renewal memberships with result: Success ",
]);

$message = ts('Membership Renewals Processing Completed');
CRM_Core_Session::setStatus($message, '', 'success');
}

/**
* PostRunAllTask will run as the last task in the queue. Leave
* CRM_MembershipExtras_Queue_Task_PostRunAll::process empty if there is no
* need for it because queue web-runner will show 'Done' message instead of
* the last task title.
*/
protected function addPostRunAllTask() {
$taskTitle = 'Done';
$records = [1];

$task = new CRM_Queue_Task(
[PostRunAllTask::class, 'run'],
[$records],
$taskTitle
);

$this->queue->createItem($task);
}

}
50 changes: 50 additions & 0 deletions CRM/MembershipExtras/Queue/Builder/Base.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

abstract class CRM_MembershipExtras_Queue_Builder_Base {
ahed-compucorp marked this conversation as resolved.
Show resolved Hide resolved

public const RECORDS_LIMIT = 10;

protected $queue;

protected $records = [];

protected $taskCallback = [];

/**
* CRM_MembershipExtras_Queue_Build_BaseBuilder constructor.
*
* @param CRM_Queue_Queue $queue
*/
public function __construct($queue) {
$this->queue = $queue;
}

protected function buildQueue($records) {
foreach ($records as $record) {
if (count($this->records) >= self::RECORDS_LIMIT) {
$this->addQueueTaskItem();
$this->records = [];
}

$this->records[] = $record;
}

if (!empty($this->records)) {
$this->addQueueTaskItem();
}
}

protected function addQueueTaskItem() {
$records = implode(', ', $this->records);
$taskTitle = sprintf('Processing the records: %s', $records);

$task = new CRM_Queue_Task(
$this->taskCallback,
[$this->records],
$taskTitle
);

$this->queue->createItem($task);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

use CRM_MembershipExtras_Queue_Builder_Base as BaseQueueBuilder;
use CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_PaymentPlanTrait as OfflineAutoRenewalPaymentPlanQueueBuilderTrait;
use CRM_MembershipExtras_Queue_Task_OfflineAutoRenewal_RenewMultipleInstalmentPlans as OfflineRenewMultipleInstalmentPlansTask;

/**
* Adds payment plans with multiple instalments that are ready to
* be renewed to the queue.
*/
class CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_MultipleInstalmentPlan extends BaseQueueBuilder {
use OfflineAutoRenewalPaymentPlanQueueBuilderTrait;

protected $taskCallback = [OfflineRenewMultipleInstalmentPlansTask::class, 'run'];

public function run() {
$records = $this->getRecurringContributions();
$this->buildQueue($records);
}

/**
* Returns a list of payment plans with multiple instalments that have at
* least one line item ready to be renewed (ie. has an end date, is not
* removed and is set to auto renew), mmeting these conditions:
*
* 1- is using an offline payment processor (payment manual class).
* 2- has an end date.
* 3- is set to auto-renew
* 4- is not in status cancelled
* 5- "Next Payment Plan Period" is empty
* 6- has either of the following conditions:
* - end date of at least one membership is equal to or smaller than today
* - there are no related line items with memberships to be renewed and
* line items have an end date
*
* @return array
*/
private function getRecurringContributions() {
$manualPaymentProcessorsIDs = implode(',', $this->manualPaymentProcessorIDs);
$cancelledStatusID = $this->contributionStatusesNameMap['Cancelled'];
$refundedStatusID = $this->contributionStatusesNameMap['Refunded'];
$daysToRenewInAdvance = $this->daysToRenewInAdvance;

$query = "
SELECT ccr.id as contribution_recur_id
FROM civicrm_contribution_recur ccr
LEFT JOIN membershipextras_subscription_line msl ON msl.contribution_recur_id = ccr.id
LEFT JOIN civicrm_line_item cli ON msl.line_item_id = cli.id
LEFT JOIN civicrm_membership cm ON (cm.id = cli.entity_id AND cli.entity_table = 'civicrm_membership')
LEFT JOIN civicrm_value_payment_plan_periods ppp ON ppp.entity_id = ccr.id
WHERE (ccr.payment_processor_id IS NULL OR ccr.payment_processor_id IN ({$manualPaymentProcessorsIDs}))
AND ccr.installments > 1
AND ccr.auto_renew = 1
AND (
ccr.contribution_status_id != {$cancelledStatusID}
AND ccr.contribution_status_id != {$refundedStatusID}
)
AND (ppp.next_period IS NULL OR ppp.next_period = 0)
AND msl.auto_renew = 1
AND msl.is_removed = 0
GROUP BY ccr.id
HAVING MIN(cm.end_date) <= DATE_ADD(CURDATE(), INTERVAL {$daysToRenewInAdvance} DAY)
OR (
COUNT(cm.id) = 0
AND COUNT(msl.id) > 0
)
";
$recurContributions = CRM_Core_DAO::executeQuery($query);

$recurContributionIDs = [];
while ($recurContributions->fetch()) {
$recurContributionIDs[] = $recurContributions->contribution_recur_id;
}

return $recurContributionIDs;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/**
* Trait CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_PaymentPlanTrait
*/
trait CRM_MembershipExtras_Queue_Builder_OfflineAutoRenewal_PaymentPlanTrait {

/**
* Maps contribution status names to their corresponding ID's.
*
* @var array
*/
protected $contributionStatusesNameMap;

/**
* Number of days in advance a membership shuld be renewed.
*
* @var int
*/
protected $daysToRenewInAdvance;

/**
* ID's for payment processors that are considered to be manual.
*
* @var array
*/
protected $manualPaymentProcessorIDs;

/**
* CRM_MembershipExtras_Queue_Builder_MultipleInstalmentPlans constructor.
*
* @throws \CiviCRM_API3_Exception
*/
public function __construct($queue) {
parent::__construct($queue);

$this->setContributionStatusesNameMap();
$this->setManualPaymentProcessorIDs();
$this->setDaysToRenewInAdvance();
}

/**
* Gets contribution Statuses Name to value Mapping
*
* @throws \CiviCRM_API3_Exception
*/
private function setContributionStatusesNameMap() {
$contributionStatuses = civicrm_api3('OptionValue', 'get', [
'sequential' => 1,
'return' => ['name', 'value'],
'option_group_id' => 'contribution_status',
'options' => ['limit' => 0],
])['values'];

$contributionStatusesNameMap = [];
foreach ($contributionStatuses as $status) {
$contributionStatusesNameMap[$status['name']] = $status['value'];
}

$this->contributionStatusesNameMap = $contributionStatusesNameMap;
}

/**
* Loads setting and assigns it to a class attribute.
*/
private function setDaysToRenewInAdvance() {
$this->daysToRenewInAdvance = CRM_MembershipExtras_SettingsManager::getDaysToRenewInAdvance();
}

/**
* Loads list of manual payment processors into an array as a class attribute.
*/
private function setManualPaymentProcessorIDs() {
$payLaterProcessorID = 0;
$this->manualPaymentProcessorIDs = array_merge([$payLaterProcessorID], CRM_MembershipExtras_Service_ManualPaymentProcessors::getIDs());
}

}
Loading