/** * Will acquire a lock with the given name, * if no other lock has been acquired by this process. * * If the same lock has been acquired before (and not been released), * in internal counter is increased. Therefore you can acquire the same * lock multiple times, but you will then have to release them * the same amount of times * * @return a SafeLock instance or NULL if timed out */ public static function acquireLock($name, $timeout = 60) { if (self::$_acquired_lock == NULL) { // it's free, we'll try to take it $lock = new CRM_Core_Lock($name, $timeout); if (version_compare(CRM_Utils_System::version(), '4.6', '>=')) { // before 4.6, a new lock would be automatically acquired $lock->acquire(); } if ($lock != NULL && $lock->isAcquired()) { // we got it! self::$_acquired_lock = new CRM_Utils_SepaSafeLock($lock, $name); //error_log('acquired ' . getmypid()); return self::$_acquired_lock; } else { // timed out return NULL; } } elseif (self::$_acquired_lock->getName() == $name) { // this means acquiring 'our' lock again: $lock = self::$_acquired_lock; $lock->counter += 1; //error_log('acquired ' . getmypid() . "[{$lock->counter}]"); return $lock; } else { // this is the BAD case: somebody's trying to acquire ANOTHER LOCK, // while we still own another one $lock_name = $self::$_acquired_lock->getName(); throw new Exception("This process cannot acquire more than one lock! It still owns lock '{$lock_name}'."); } }
/** * Mailjet.ProcessBounces API * * @param array $params * @return array API result descriptor * @see civicrm_api3_create_success * @see civicrm_api3_create_error * @throws API_Exception */ function civicrm_api3_mailjet_processbounces($params) { $lock = new CRM_Core_Lock('civimail.job.MailjetProcessor'); if (!$lock->isAcquired()) { return civicrm_api3_create_error('Could not acquire lock, another MailjetProcessor process is running'); } $mailingId = CRM_Utils_Array::value('mailing_id', $params); //G: this is called when click on "Manually refresh Mailjet's stats" button if (!CRM_Utils_Mail_MailjetProcessor::processBounces($mailingId)) { $lock->release(); return civicrm_api3_create_error('Process Bounces failed'); } $lock->release(); // FIXME: processBounces doesn't return true/false on success/failure $values = array(); return civicrm_api3_create_success($values, $params, 'mailjet', 'bounces'); }
function run() { require_once 'CRM/Core/Lock.php'; $lock = new CRM_Core_Lock('CiviReportMail'); if ($lock->isAcquired()) { // try to unset any time limits if (!ini_get('safe_mode')) { set_time_limit(0); } // if there are named sets of settings, use them - otherwise use the default (null) require_once 'CRM/Report/Utils/Report.php'; $result = CRM_Report_Utils_Report::processReport(); echo $result['messages']; } else { throw new Exception('Could not acquire lock, another CiviReportMail process is running'); } $lock->release(); }
} else { CRM_Utils_Mail_EmailProcessor::processBounces(); } $lock->release(); } else { session_start(); require_once '../civicrm.config.php'; require_once 'CRM/Core/Config.php'; $config = CRM_Core_Config::singleton(); CRM_Utils_System::authenticateScript(TRUE); require_once 'CRM/Utils/System.php'; CRM_Utils_System::loadBootStrap(); //log the execution of script CRM_Core_Error::debug_log_message('EmailProcessor.php'); require_once 'CRM/Core/Lock.php'; $lock = new CRM_Core_Lock('EmailProcessor'); if (!$lock->isAcquired()) { throw new Exception('Could not acquire lock, another EmailProcessor process is running'); } // try to unset any time limits if (!ini_get('safe_mode')) { set_time_limit(0); } require_once 'CRM/Utils/Mail/EmailProcessor.php'; // cleanup directories with old mail files (if they exist): CRM-4452 CRM_Utils_Mail_EmailProcessor::cleanupDir($config->customFileUploadDir . DIRECTORY_SEPARATOR . 'CiviMail.ignored'); CRM_Utils_Mail_EmailProcessor::cleanupDir($config->customFileUploadDir . DIRECTORY_SEPARATOR . 'CiviMail.processed'); // check if the script is being used for civimail processing or email to // activity processing. $isCiviMail = !empty($_REQUEST['emailtoactivity']) ? FALSE : TRUE; CRM_Utils_Mail_EmailProcessor::process($isCiviMail);
/** * before we run jobs, we need to split the jobs * @param int $offset * @param null $mode */ public static function runJobs_pre($offset = 200, $mode = NULL) { $job = new CRM_Mailing_BAO_MailingJob(); $jobTable = CRM_Mailing_DAO_MailingJob::getTableName(); $mailingTable = CRM_Mailing_DAO_Mailing::getTableName(); $currentTime = date('YmdHis'); $mailingACL = CRM_Mailing_BAO_Mailing::mailingACL('m'); $workflowClause = CRM_Mailing_BAO_MailingJob::workflowClause(); $domainID = CRM_Core_Config::domainID(); $modeClause = 'AND m.sms_provider_id IS NULL'; if ($mode == 'sms') { $modeClause = 'AND m.sms_provider_id IS NOT NULL'; } // Select all the mailing jobs that are created from // when the mailing is submitted or scheduled. $query = "\n SELECT j.*\n FROM {$jobTable} j,\n {$mailingTable} m\n WHERE m.id = j.mailing_id AND m.domain_id = {$domainID}\n {$workflowClause}\n {$modeClause}\n AND j.is_test = 0\n AND ( ( j.start_date IS null\n AND j.scheduled_date <= {$currentTime}\n AND j.status = 'Scheduled'\n AND j.end_date IS null ) )\n AND ((j.job_type is NULL) OR (j.job_type <> 'child'))\n ORDER BY j.scheduled_date,\n j.start_date"; $job->query($query); // For each of the "Parent Jobs" we find, we split them into // X Number of child jobs while ($job->fetch()) { // still use job level lock for each child job $lockName = "civimail.job.{$job->id}"; $lock = new CRM_Core_Lock($lockName); if (!$lock->isAcquired()) { continue; } // Re-fetch the job status in case things // changed between the first query and now // to avoid race conditions $job->status = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingJob', $job->id, 'status', 'id', TRUE); if ($job->status != 'Scheduled') { $lock->release(); continue; } $job->split_job($offset); // update the status of the parent job $transaction = new CRM_Core_Transaction(); $saveJob = new CRM_Mailing_DAO_MailingJob(); $saveJob->id = $job->id; $saveJob->start_date = date('YmdHis'); $saveJob->status = 'Running'; $saveJob->save(); $transaction->commit(); // Release the job lock $lock->release(); } }
/** * load the smart group cache for a saved search * * @param object $group - the smart group that needs to be loaded * @param boolean $force - should we force a search through * */ static function load(&$group, $force = FALSE) { $groupID = $group->id; $savedSearchID = $group->saved_search_id; if (array_key_exists($groupID, self::$_alreadyLoaded) && !$force) { return; } // grab a lock so other processes dont compete and do the same query $lockName = "civicrm.group.{$groupID}"; $lock = new CRM_Core_Lock($lockName); if (!$lock->isAcquired()) { // this can cause inconsistent results since we dont know if the other process // will fill up the cache before our calling routine needs it. // however this routine does not return the status either, so basically // its a "lets return and hope for the best" return; } self::$_alreadyLoaded[$groupID] = 1; // we now have the lock, but some other proces could have actually done the work // before we got here, so before we do any work, lets ensure that work needs to be // done // we allow hidden groups here since we dont know if the caller wants to evaluate an // hidden group if (!$force && !self::shouldGroupBeRefreshed($groupID, TRUE)) { $lock->release(); return; } $sql = NULL; $idName = 'id'; $customClass = NULL; if ($savedSearchID) { $ssParams = CRM_Contact_BAO_SavedSearch::getSearchParams($savedSearchID); // rectify params to what proximity search expects if there is a value for prox_distance // CRM-7021 if (!empty($ssParams)) { CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams); } $returnProperties = array(); if (CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $savedSearchID, 'mapping_id')) { $fv = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID); $returnProperties = CRM_Core_BAO_Mapping::returnProperties($fv); } if (isset($ssParams['customSearchID'])) { // if custom search // we split it up and store custom class // so temp tables are not destroyed if they are used // hence customClass is defined above at top of function $customClass = CRM_Contact_BAO_SearchCustom::customClass($ssParams['customSearchID'], $savedSearchID); $searchSQL = $customClass->contactIDs(); $searchSQL = str_replace('ORDER BY contact_a.id ASC', '', $searchSQL); $idName = 'contact_id'; } else { $formValues = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID); $query = new CRM_Contact_BAO_Query($ssParams, $returnProperties, NULL, FALSE, FALSE, 1, TRUE, TRUE, FALSE, CRM_Utils_Array::value('display_relationship_type', $formValues), CRM_Utils_Array::value('operator', $formValues, 'AND')); $query->_useDistinct = FALSE; $query->_useGroupBy = FALSE; $searchSQL = $query->searchQuery(0, 0, NULL, FALSE, FALSE, FALSE, TRUE, TRUE, NULL, NULL, NULL, TRUE); } $groupID = CRM_Utils_Type::escape($groupID, 'Integer'); $sql = $searchSQL . " AND contact_a.id NOT IN (\n SELECT contact_id FROM civicrm_group_contact\n WHERE civicrm_group_contact.status = 'Removed'\n AND civicrm_group_contact.group_id = {$groupID} ) "; } if ($sql) { $sql = preg_replace("/^\\s*SELECT/", "SELECT {$groupID} as group_id, ", $sql); } // lets also store the records that are explicitly added to the group // this allows us to skip the group contact LEFT JOIN $sqlB = "\nSELECT {$groupID} as group_id, contact_id as {$idName}\nFROM civicrm_group_contact\nWHERE civicrm_group_contact.status = 'Added'\n AND civicrm_group_contact.group_id = {$groupID} "; $groupIDs = array($groupID); self::remove($groupIDs); $processed = FALSE; $tempTable = 'civicrm_temp_group_contact_cache' . rand(0, 2000); foreach (array($sql, $sqlB) as $selectSql) { if (!$selectSql) { continue; } $insertSql = "CREATE TEMPORARY TABLE {$tempTable} ({$selectSql});"; $processed = TRUE; $result = CRM_Core_DAO::executeQuery($insertSql); CRM_Core_DAO::executeQuery("INSERT IGNORE INTO civicrm_group_contact_cache (contact_id, group_id)\n SELECT DISTINCT {$idName}, group_id FROM {$tempTable}\n "); CRM_Core_DAO::executeQuery(" DROP TABLE {$tempTable}"); } self::updateCacheTime($groupIDs, $processed); if ($group->children) { //Store a list of contacts who are removed from the parent group $sql = "\nSELECT contact_id\nFROM civicrm_group_contact\nWHERE civicrm_group_contact.status = 'Removed'\nAND civicrm_group_contact.group_id = {$groupID} "; $dao = CRM_Core_DAO::executeQuery($sql); $removed_contacts = array(); while ($dao->fetch()) { $removed_contacts[] = $dao->contact_id; } $childrenIDs = explode(',', $group->children); foreach ($childrenIDs as $childID) { $contactIDs = CRM_Contact_BAO_Group::getMember($childID, FALSE); //Unset each contact that is removed from the parent group foreach ($removed_contacts as $removed_contact) { unset($contactIDs[$removed_contact]); } $values = array(); foreach ($contactIDs as $contactID => $dontCare) { $values[] = "({$groupID},{$contactID})"; } self::store($groupIDs, $values); } } $lock->release(); }
static function processQueue($mode = NULL) { $config =& CRM_Core_Config::singleton(); // CRM_Core_Error::debug_log_message("Beginning processQueue run: {$config->mailerJobsMax}, {$config->mailerJobSize}"); if ($mode == NULL && CRM_Core_BAO_MailSettings::defaultDomain() == "EXAMPLE.ORG") { CRM_Core_Error::fatal(ts('The <a href="%1">default mailbox</a> has not been configured. You will find <a href="%2">more info in the online user and administrator guide</a>', array(1 => CRM_Utils_System::url('civicrm/admin/mailSettings', 'reset=1'), 2 => "http://book.civicrm.org/user/advanced-configuration/email-system-configuration/"))); } // check if we are enforcing number of parallel cron jobs // CRM-8460 $gotCronLock = FALSE; if (property_exists($config, 'mailerJobsMax') && $config->mailerJobsMax && $config->mailerJobsMax > 1) { $lockArray = range(1, $config->mailerJobsMax); shuffle($lockArray); // check if we are using global locks $serverWideLock = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, 'civimail_server_wide_lock'); foreach ($lockArray as $lockID) { $cronLock = new CRM_Core_Lock("civimail.cronjob.{$lockID}", NULL, $serverWideLock); if ($cronLock->isAcquired()) { $gotCronLock = TRUE; break; } } // exit here since we have enuf cronjobs running if (!$gotCronLock) { CRM_Core_Error::debug_log_message('Returning early, since max number of cronjobs running'); return TRUE; } } // load bootstrap to call hooks // Split up the parent jobs into multiple child jobs $mailerJobSize = property_exists($config, 'mailerJobSize') ? $config->mailerJobSize : NULL; CRM_Mailing_BAO_MailingJob::runJobs_pre($mailerJobSize, $mode); CRM_Mailing_BAO_MailingJob::runJobs(NULL, $mode); CRM_Mailing_BAO_MailingJob::runJobs_post($mode); // lets release the global cron lock if we do have one if ($gotCronLock) { $cronLock->release(); } // CRM_Core_Error::debug_log_message('Ending processQueue run'); return TRUE; }
/** * This api reloads all the smart groups. If the org has a large number of smart groups * it is recommended that they use the limit clause to limit the number of smart groups * evaluated on a per job basis. Might also help to increase the smartGroupCacheTimeout * and use the cache */ function civicrm_api3_job_group_rebuild($params) { $lock = new CRM_Core_Lock('civimail.job.groupRebuild'); if (!$lock->isAcquired()) { return civicrm_api3_create_error('Could not acquire lock, another EmailProcessor process is running'); } $limit = CRM_Utils_Array::value('limit', $params, 0); CRM_Contact_BAO_GroupContactCache::loadAll(null, $limit); $lock->release(); return civicrm_api3_create_success(); }
/** * @return null|string */ public function release() { if ($this->_hasLock) { if (defined('CIVICRM_LOCK_DEBUG')) { CRM_Core_Error::debug_log_message('release lock for ' . $this->_name); } $this->_hasLock = FALSE; if (self::$jobLog == $this->_name) { self::$jobLog = FALSE; } $query = "SELECT RELEASE_LOCK( %1 )"; $params = array(1 => array($this->_name, 'String')); return CRM_Core_DAO::singleValueQuery($query, $params); } }
/** * Initiate all pending/ready jobs * * @return void * @access public * @static */ public static function runJobs($testParams = null) { $job =& new CRM_Mailing_BAO_Job(); $mailing =& new CRM_Mailing_DAO_Mailing(); $config =& CRM_Core_Config::singleton(); $jobTable = CRM_Mailing_DAO_Job::getTableName(); $mailingTable = CRM_Mailing_DAO_Mailing::getTableName(); if (!empty($testParams)) { $query = "\nSELECT *\n FROM {$jobTable}\n WHERE id = {$testParams['job_id']}"; $job->query($query); } else { $currentTime = date('YmdHis'); /* FIXME: we might want to go to a progress table.. */ $query = "\nSELECT j.*\n FROM {$jobTable} j,\n {$mailingTable} m\n WHERE m.id = j.mailing_id\n AND j.is_test = 0\n AND ( ( j.start_date IS null\n AND j.scheduled_date <= {$currentTime}\n AND j.status = 'Scheduled' )\n OR ( j.status = 'Running'\n AND j.end_date IS null ) )\nORDER BY j.scheduled_date,\n j.start_date"; $job->query($query); } require_once 'CRM/Core/Lock.php'; /* TODO We should parallelize or prioritize this */ while ($job->fetch()) { // fix for cancel job at run time which is in queue, CRM-4246 if (CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Job', $job->id, 'status') == 'Canceled') { continue; } $lockName = "civimail.job.{$job->id}"; // get a lock on this job id $lock = new CRM_Core_Lock($lockName); if (!$lock->isAcquired()) { continue; } /* Queue up recipients for all jobs being launched */ if ($job->status != 'Running') { require_once 'CRM/Core/Transaction.php'; $transaction = new CRM_Core_Transaction(); $job->queue($testParams); /* Start the job */ // use a seperate DAO object to protect the loop // integrity. I think transactions messes it up // check CRM-2469 $saveJob = new CRM_Mailing_DAO_Job(); $saveJob->id = $job->id; $saveJob->start_date = date('YmdHis'); $saveJob->status = 'Running'; $saveJob->save(); $transaction->commit(); } $mailer =& $config->getMailer(); /* Compose and deliver */ $isComplete = $job->deliver($mailer, $testParams); require_once 'CRM/Utils/Hook.php'; CRM_Utils_Hook::post('create', 'CRM_Mailing_DAO_Spool', $job->id, $isComplete); if ($isComplete) { /* Finish the job */ require_once 'CRM/Core/Transaction.php'; $transaction = new CRM_Core_Transaction(); // use a seperate DAO object to protect the loop // integrity. I think transactions messes it up // check CRM-2469 $saveJob = new CRM_Mailing_DAO_Job(); $saveJob->id = $job->id; $saveJob->end_date = date('YmdHis'); $saveJob->status = 'Complete'; $saveJob->save(); $mailing->reset(); $mailing->id = $job->mailing_id; $mailing->is_completed = true; $mailing->save(); $transaction->commit(); } $lock->release(); if ($testParams) { return $isComplete; } } }
function civicrm_api3_job_fetch_activities($params) { require_once 'CRM/Utils/Mail/EmailProcessor.php'; require_once 'CRM/Core/Lock.php'; $lock = new CRM_Core_Lock('EmailProcessor'); if (!$lock->isAcquired()) { return civicrm_api3_create_error("Could not acquire lock, another EmailProcessor process is running"); } try { CRM_Utils_Mail_EmailProcessor::processActivities(); $values = array(); $lock->release(); return civicrm_api3_create_success($values, $params, 'mailing', 'activities'); } catch (Exception $e) { $lock->release(); return civicrm_api3_create_error("Process Activities failed"); } }
/** * Store an item in the DB cache * * @param object $data (required) A reference to the data that will be serialized and stored * @param string $group (required) The group name of the item * @param string $path (required) The path under which this item is stored * @param int $componentID The optional component ID (so componenets can share the same name space) * * @return void * @static * @access public */ static function setItem(&$data, $group, $path, $componentID = NULL) { if (self::$_cache === NULL) { self::$_cache = array(); } $dao = new CRM_Core_DAO_Cache(); $dao->group_name = $group; $dao->path = $path; $dao->component_id = $componentID; // get a lock so that multiple ajax requests on the same page // dont trample on each other // CRM-11234 $lockName = "civicrm.cache.{$group}_{$path}._{$componentID}"; $lock = new CRM_Core_Lock($lockName); if (!$lock->isAcquired()) { CRM_Core_Error::fatal(); } $dao->find(TRUE); $dao->data = serialize($data); $dao->created_date = date('YmdHis'); $dao->save(); $lock->release(); $dao->free(); // cache coherency - refresh or remove dependent caches $argString = "CRM_CT_{$group}_{$path}_{$componentID}"; $cache = CRM_Utils_Cache::singleton(); $data = unserialize($dao->data); self::$_cache[$argString] = $data; $cache->set($argString, $data); $argString = "CRM_CT_CI_{$group}_{$componentID}"; unset(self::$_cache[$argString]); $cache->delete($argString); }
public static function runJobs_pre($offset = 200) { $job = new CRM_Mailing_BAO_Job(); $config = CRM_Core_Config::singleton(); $jobTable = CRM_Mailing_DAO_Job::getTableName(); $mailingTable = CRM_Mailing_DAO_Mailing::getTableName(); $currentTime = date('YmdHis'); $mailingACL = CRM_Mailing_BAO_Mailing::mailingACL('m'); // add an additional check and only process // jobs that are approved $workflowClause = null; require_once 'CRM/Mailing/Info.php'; if (CRM_Mailing_Info::workflowEnabled()) { require_once 'CRM/Core/OptionGroup.php'; $approveOptionID = CRM_Core_OptionGroup::getValue('mail_approval_status', 'Approved', 'name'); if ($approveOptionID) { $workflowClause = " AND m.approval_status_id = {$approveOptionID} "; } } // Select all the mailing jobs that are created from // when the mailing is submitted or scheduled. $query = "\n\t\tSELECT j.*\n\t\t FROM {$jobTable} j,\n\t\t\t\t {$mailingTable} m\n\t\t WHERE m.id = j.mailing_id\n {$workflowClause}\n\t\t AND j.is_test = 0\n\t\t AND ( ( j.start_date IS null\n\t\t AND j.scheduled_date <= {$currentTime}\n\t\t AND j.status = 'Scheduled'\n\t\t AND j.end_date IS null ) )\n\t\t AND ((j.job_type is NULL) OR (j.job_type <> 'child'))\n\t\tORDER BY j.scheduled_date,\n\t\t\t\t j.start_date"; $job->query($query); require_once 'CRM/Core/Lock.php'; // For reach of the "Parent Jobs" we find, we split them into // X Number of child jobs while ($job->fetch()) { // still use job level lock for each child job $lockName = "civimail.job.{$job->id}"; $lock = new CRM_Core_Lock($lockName); if (!$lock->isAcquired()) { continue; } // refetch the job status in case things // changed between the first query and now // avoid race conditions $job->status = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Job', $job->id, 'status'); if ($job->status != 'Scheduled') { $lock->release(); continue; } $job->split_job($offset); // update the status of the parent job require_once 'CRM/Core/Transaction.php'; $transaction = new CRM_Core_Transaction(); $saveJob = new CRM_Mailing_DAO_Job(); $saveJob->id = $job->id; $saveJob->start_date = date('YmdHis'); $saveJob->status = 'Running'; $saveJob->save(); $transaction->commit(); // Release the job lock $lock->release(); } }
/** * Job.iATSRecurringContributions API * * @param array $params * @return array API result descriptor * @see civicrm_api3_create_success * @see civicrm_api3_create_error * @throws API_Exception */ function civicrm_api3_job_iatsrecurringcontributions($params) { // running this job in parallell could generate bad duplicate contributions $lock = new CRM_Core_Lock('civimail.job.IatsRecurringContributions'); if (!$lock->acquire()) { return civicrm_api3_create_success(ts('Failed to acquire lock. No contribution records were processed.')); } $catchup = !empty($params['catchup']); unset($params['catchup']); $domemberships = empty($params['ignoremembership']); unset($params['ignoremembership']); // TODO: what kind of extra security do we want or need here to prevent it from being triggered inappropriately? Or does it matter? // the next scheduled contribution date field name is civicrm version dependent define('IATS_CIVICRM_NSCD_FID', _iats_civicrm_nscd_fid()); // $config = &CRM_Core_Config::singleton(); // $debug = false; // do my calculations based on yyyymmddhhmmss representation of the time // not sure about time-zone issues $dtCurrentDay = date("Ymd", mktime(0, 0, 0, date("m"), date("d"), date("Y"))); $dtCurrentDayStart = $dtCurrentDay . "000000"; $dtCurrentDayEnd = $dtCurrentDay . "235959"; $expiry_limit = date('ym'); // restrict this method of recurring contribution processing to only these two payment processors $args = array(1 => array('Payment_iATSService', 'String'), 2 => array('Payment_iATSServiceACHEFT', 'String'), 3 => array('Payment_iATSServiceSWIPE', 'String')); // Before triggering payments, we need to do some housekeeping of the civicrm_contribution_recur records. // First update the end_date and then the complete/in-progress values. // We do this both to fix any failed settings previously, and also // to deal with the possibility that the settings for the number of payments (installments) for an existing record has changed. // First check for recur end date values on non-open-ended recurring contribution records that are either complete or in-progress $select = 'SELECT cr.id, count(c.id) AS installments_done, cr.installments, cr.end_date, NOW() as test_now FROM civicrm_contribution_recur cr INNER JOIN civicrm_contribution c ON cr.id = c.contribution_recur_id INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id WHERE (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3) AND (cr.installments > 0) AND (cr.contribution_status_id IN (1,5)) AND (c.contribution_status_id IN (1,2)) GROUP BY c.contribution_recur_id'; $dao = CRM_Core_DAO::executeQuery($select, $args); while ($dao->fetch()) { // check for end dates that should be unset because I haven't finished if ($dao->installments_done < $dao->installments) { // at least one more installment todo if ($dao->end_date > 0 && $dao->end_date <= $dao->test_now) { // unset the end_date $update = 'UPDATE civicrm_contribution_recur SET end_date = NULL, contribution_status_id = 5 WHERE id = %1'; CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int'))); } } elseif ($dao->installments_done >= $dao->installments) { // I'm done with installments if (empty($dao->end_date) || $dao->end_date >= $dao->test_now) { // this interval complete, set the end_date to an hour ago $update = 'UPDATE civicrm_contribution_recur SET end_date = DATE_SUB(NOW(),INTERVAL 1 HOUR) WHERE id = %1'; CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int'))); } } } // Second, make sure any open-ended recurring contributions have no end date set $update = 'UPDATE civicrm_contribution_recur cr INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id SET cr.end_date = NULL WHERE cr.contribution_status_id IN (1,5) AND NOT(cr.installments > 0) AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3) AND NOT(ISNULL(cr.end_date))'; $dao = CRM_Core_DAO::executeQuery($update, $args); // Third, we update the status_id of the all in-progress or completed recurring contribution records // Unexpire uncompleted cycles $update = 'UPDATE civicrm_contribution_recur cr INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id SET cr.contribution_status_id = 5 WHERE cr.contribution_status_id = 1 AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3) AND (cr.end_date IS NULL OR cr.end_date > NOW())'; $dao = CRM_Core_DAO::executeQuery($update, $args); // Expire or badly-defined completed cycles $update = 'UPDATE civicrm_contribution_recur cr INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id SET cr.contribution_status_id = 1 WHERE cr.contribution_status_id = 5 AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3) AND ( (NOT(cr.end_date IS NULL) AND cr.end_date <= NOW()) OR ISNULL(cr.frequency_unit) OR (frequency_interval = 0) )'; $dao = CRM_Core_DAO::executeQuery($update, $args); // Now we're ready to trigger payments // Select the ongoing recurring payments for iATSServices where the next scheduled contribution date (NSCD) is before the end of of the current day $select = 'SELECT cr.*, icc.customer_code, icc.expiry as icc_expiry, icc.cid as icc_contact_id, pp.class_name as pp_class_name, pp.url_site as url_site, pp.is_test FROM civicrm_contribution_recur cr INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id INNER JOIN civicrm_iats_customer_codes icc ON cr.id = icc.recur_id WHERE cr.contribution_status_id = 5 AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)'; // AND pp.is_test = 0 if (!empty($params['recur_id'])) { // in case the job was called to execute a specific recurring contribution id -- not yet implemented! $select .= ' AND icc.recur_id = %4'; $args[4] = array($params['recur_id'], 'Int'); } else { // if (!empty($params['scheduled'])) { //normally, process all recurring contributions due today or earlier $select .= ' AND cr.' . IATS_CIVICRM_NSCD_FID . ' <= %4'; $args[4] = array($dtCurrentDayEnd, 'String'); // ' AND cr.next_sched_contribution >= %2 // $args[2] = array($dtCurrentDayStart, 'String'); if (!empty($params['cycle_day'])) { // also filter by cycle day $select .= ' AND cr.cycle_day = %5'; $args[5] = array($params['cycle_day'], 'Int'); } if (isset($params['failure_count'])) { // also filter by cycle day $select .= ' AND cr.failure_count = %6'; $args[6] = array($params['failure_count'], 'Int'); } } $dao = CRM_Core_DAO::executeQuery($select, $args); $counter = 0; $error_count = 0; $output = array(); $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings'); $receipt_recurring = empty($settings['receipt_recurring']) ? 0 : 1; /* while ($dao->fetch()) { foreach($dao as $key => $value) { echo "$value,"; } echo "\n"; } die(); */ while ($dao->fetch()) { // Strategy: create the contribution record with status = 2 (= pending), try the payment, and update the status to 1 if successful // Try to get a contribution template for this contribution series - if none matches (e.g. if a donation amount has been changed), we'll just be naive about it. $contribution_template = _iats_civicrm_getContributionTemplate(array('contribution_recur_id' => $dao->id, 'total_amount' => $dao->amount)); $contact_id = $dao->contact_id; $total_amount = $dao->amount; $hash = md5(uniqid(rand(), true)); $contribution_recur_id = $dao->id; $subtype = substr($dao->pp_class_name, 19); $source = "iATS Payments {$subtype} Recurring Contribution (id={$contribution_recur_id})"; $receive_ts = $catchup ? strtotime($dao->next_sched_contribution_date) : time(); $receive_date = date("YmdHis", $receive_ts); // i.e. now or whenever it was supposed to run if in catchup mode // check if we already have an error $errors = array(); if (empty($dao->customer_code)) { $errors[] = ts('Recur id %1 is missing a customer code.', array(1 => $contribution_recur_id)); } else { if ($dao->contact_id != $dao->icc_contact_id) { $errors[] = ts('Recur id %1 is has a mismatched contact id for the customer code.', array(1 => $contribution_recur_id)); } if ($dao->icc_expiry != '0000' && $dao->icc_expiry < $expiry_limit) { // $errors[] = ts('Recur id %1 is has an expired cc for the customer code.', array(1 => $contribution_recur_id)); } } if (count($errors)) { $source .= ' Errors: ' . implode(' ', $errors); } $contribution = array('version' => 3, 'contact_id' => $contact_id, 'receive_date' => $receive_date, 'total_amount' => $total_amount, 'payment_instrument_id' => $dao->payment_instrument_id, 'contribution_recur_id' => $contribution_recur_id, 'invoice_id' => $hash, 'source' => $source, 'contribution_status_id' => 2, 'currency' => $dao->currency, 'payment_processor' => $dao->payment_processor_id, 'is_test' => $dao->is_test); $get_from_template = array('contribution_campaign_id', 'amount_level'); foreach ($get_from_template as $field) { if (isset($contribution_template[$field])) { $contribution[$field] = is_array($contribution_template[$field]) ? implode(', ', $contribution_template[$field]) : $contribution_template[$field]; } } if (isset($dao->contribution_type_id)) { // 4.2 $contribution['contribution_type_id'] = $dao->contribution_type_id; } else { // 4.3+ $contribution['financial_type_id'] = $dao->financial_type_id; } if (!empty($contribution_template['line_items'])) { $contribution['skipLineItem'] = 1; $contribution['api.line_item.create'] = $contribution_template['line_items']; } if (count($errors)) { ++$error_count; ++$counter; /* create a failed contribution record, don't bother talking to iats */ $contribution['contribution_status_id'] = 4; $contributionResult = civicrm_api('contribution', 'create', $contribution); if ($contributionResult['is_error']) { $errors[] = $contributionResult['error_message']; } continue; } else { // assign basic options $options = array('is_email_receipt' => $receipt_recurring, 'customer_code' => $dao->customer_code, 'subtype' => $subtype); // if our template contribution is a membership payment, make this one also if ($domemberships && !empty($contribution_template['contribution_id'])) { try { $membership_payment = civicrm_api('MembershipPayment', 'getsingle', array('version' => 3, 'contribution_id' => $contribution_template['contribution_id'])); if (!empty($membership_payment['membership_id'])) { $options['membership_id'] = $membership_payment['membership_id']; } } catch (Exception $e) { // ignore, if will fail correctly if there is no membership payment } } // so far so, good ... now create the pending contribution, and save its id // and then try to get the money, and do one of: update the contribution to failed, complete the transaction, or update a pending ach/eft with it's transaction id $output[] = _iats_process_contribution_payment($contribution, $options); } /* calculate the next collection date, based on the recieve date (note effect of catchup mode) */ /* only move the next sched contribution date forward if the contribution is pending (e.g. ach/eft) or complete */ if ($contribution['contribution_status_id'] < 3) { $next_collectionDate = strtotime("+{$dao->frequency_interval} {$dao->frequency_unit}", $receive_ts); $next_collectionDate = date('YmdHis', $next_collectionDate); CRM_Core_DAO::executeQuery("\n UPDATE civicrm_contribution_recur\n SET " . IATS_CIVICRM_NSCD_FID . " = %1,\n failure_count = 0\n WHERE id = %2\n ", array(1 => array($next_collectionDate, 'String'), 2 => array($dao->id, 'Int'))); } elseif (4 == $contribution['contribution_status_id']) { // i.e. failed CRM_Core_DAO::executeQuery("\n UPDATE civicrm_contribution_recur\n SET failure_count = failure_count + 1\n WHERE id = %1\n ", array(1 => array($dao->id, 'Int'))); } $result = civicrm_api('activity', 'create', array('version' => 3, 'activity_type_id' => 6, 'source_contact_id' => $contact_id, 'source_record_id' => CRM_Utils_Array::value('id', $contributionResult), 'assignee_contact_id' => $contact_id, 'subject' => "Attempted iATS Payments {$subtype} Recurring Contribution for " . $total_amount, 'status_id' => 2, 'activity_date_time' => date("YmdHis"))); if ($result['is_error']) { $output[] = ts('An error occurred while creating activity record for contact id %1: %2', array(1 => $contact_id, 2 => $result['error_message'])); ++$error_count; } else { $output[] = ts('Created activity record for contact id %1', array(1 => $contact_id)); } ++$counter; } // now update the end_dates and status for non-open-ended contribution series if they are complete (so that the recurring contribution status will show correctly) // This is a simplified version of what we did before the processing $select = 'SELECT cr.id, count(c.id) AS installments_done, cr.installments FROM civicrm_contribution_recur cr INNER JOIN civicrm_contribution c ON cr.id = c.contribution_recur_id INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id WHERE (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3) AND (cr.installments > 0) AND (cr.contribution_status_id = 5) GROUP BY c.contribution_recur_id'; $dao = CRM_Core_DAO::executeQuery($select, $args); while ($dao->fetch()) { // check if my end date should be set to now because I have finished if ($dao->installments_done >= $dao->installments) { // I'm done with installments // set this series complete and the end_date to now $update = 'UPDATE civicrm_contribution_recur SET contribution_status_id = 1, end_date = NOW() WHERE id = %1'; CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int'))); } } $lock->release(); // If errors .. if ($error_count) { return civicrm_api3_create_error(ts("Completed, but with %1 errors. %2 records processed.", array(1 => $error_count, 2 => $counter)) . "<br />" . implode("<br />", $output)); } // If no errors and records processed .. if ($counter) { return civicrm_api3_create_success(ts('%1 contribution record(s) were processed.', array(1 => $counter)) . "<br />" . implode("<br />", $output)); } // No records processed return civicrm_api3_create_success(ts('No contribution records were processed.')); }