function __construct() { self::$nscd_fid = _iats_civicrm_nscd_fid(); self::$version = _iats_civicrm_domain_info('version'); self::$financial_types = self::$version[0] <= 4 && self::$version[1] <= 2 ? array() : CRM_Contribute_PseudoConstant::financialType(); if (self::$version[0] <= 4 && self::$version[1] < 4) { self::$prefixes = CRM_Core_PseudoConstant::individualPrefix(); self::$contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(); } else { self::$prefixes = CRM_Contact_BAO_Contact::buildOptions('individual_prefix_id'); self::$contributionStatus = CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id'); } $params = array('version' => 3, 'sequential' => 1, 'is_test' => 0, 'return.name' => 1); $result = civicrm_api('PaymentProcessor', 'get', $params); foreach ($result['values'] as $pp) { self::$processors[$pp['id']] = $pp['name']; } $this->_columns = array('civicrm_contact' => array('dao' => 'CRM_Contact_DAO_Contact', 'order_bys' => array('sort_name' => array('title' => ts("Last name, First name"))), 'fields' => array('first_name' => array('title' => ts('First Name')), 'last_name' => array('title' => ts('Last Name')), 'prefix_id' => array('title' => ts('Prefix')), 'sort_name' => array('title' => ts('Contact Name'), 'no_repeat' => TRUE, 'default' => TRUE), 'id' => array('no_display' => TRUE, 'required' => TRUE))), 'civicrm_email' => array('dao' => 'CRM_Core_DAO_Email', 'order_bys' => array('email' => array('title' => ts('Email'))), 'fields' => array('email' => array('title' => ts('Email'), 'no_repeat' => TRUE)), 'grouping' => 'contact-fields'), 'civicrm_phone' => array('dao' => 'CRM_Core_DAO_Phone', 'fields' => array('phone' => array('title' => ts('Phone'), 'no_repeat' => TRUE)), 'grouping' => 'contact-fields'), 'civicrm_contribution' => array('dao' => 'CRM_Contribute_DAO_Contribution', 'fields' => array('id' => array('title' => ts('Contribution ID(s)'), 'required' => TRUE, 'dbAlias' => "GROUP_CONCAT(contribution_civireport.id SEPARATOR ', ')"), 'total_amount' => array('title' => ts('Amount Contributed to date'), 'required' => TRUE, 'statistics' => array('sum' => ts("Total Amount contributed")))), 'filters' => array('total_amount' => array('title' => ts('Total Amount'), 'operatorType' => CRM_Report_Form::OP_FLOAT, 'type' => CRM_Utils_Type::T_FLOAT))), 'civicrm_iats_customer_codes' => array('dao' => 'CRM_Contribute_DAO_Contribution', 'order_bys' => array('expiry' => array('title' => ts("Expiry Date"))), 'fields' => array('customer_code' => array('title' => 'customer code', 'default' => TRUE), 'expiry' => array('title' => 'Expiry Date', 'default' => TRUE))), 'civicrm_contribution_recur' => array('dao' => 'CRM_Contribute_DAO_ContributionRecur', 'order_bys' => array('id' => array('title' => ts("Series ID")), 'amount' => array('title' => ts("Current Amount")), 'start_date' => array('title' => ts('Start Date')), 'modified_date' => array('title' => ts('Modified Date')), self::$nscd_fid => array('title' => ts('Next Scheduled Contribution Date')), 'cycle_day' => array('title' => ts('Cycle Day')), 'payment_processor_id' => array('title' => ts('Payment Processor'))), 'fields' => array('id' => array('required' => TRUE, 'title' => ts("Series ID")), 'recur_id' => array('name' => 'id', 'title' => ts('Series ID')), 'invoice_id' => array('title' => ts('Invoice ID'), 'default' => FALSE), 'currency' => array('title' => ts("Currency")), 'amount' => array('title' => ts('Amount'), 'default' => TRUE), 'contribution_status_id' => array('title' => ts('Donation Status')), 'frequency_interval' => array('title' => ts('Frequency interval'), 'default' => TRUE), 'frequency_unit' => array('title' => ts('Frequency unit'), 'default' => TRUE), 'installments' => array('title' => ts('Installments'), 'default' => TRUE), 'start_date' => array('title' => ts('Start Date'), 'default' => TRUE), 'create_date' => array('title' => ts('Create Date')), 'modified_date' => array('title' => ts('Modified Date')), 'cancel_date' => array('title' => ts('Cancel Date')), self::$nscd_fid => array('title' => ts('Next Scheduled Contribution Date'), 'default' => TRUE), 'next_scheduled_day' => array('name' => self::$nscd_fid, 'dbAlias' => 'DAYOFMONTH(contribution_recur_civireport.next_sched_contribution)', 'title' => ts('Next Scheduled Day of the Month')), 'cycle_day' => array('title' => ts('Cycle Day')), 'failure_count' => array('title' => ts('Failure Count')), 'failure_retry_date' => array('title' => ts('Failure Retry Date')), 'payment_processor_id' => array('title' => ts('Payment Processor'), 'default' => TRUE)), 'filters' => array('contribution_status_id' => array('title' => ts('Donation Status'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, 'options' => self::$contributionStatus, 'default' => array(5), 'type' => CRM_Utils_Type::T_INT), 'payment_processor_id' => array('title' => ts('Payment Processor'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, 'options' => self::$processors, 'type' => CRM_Utils_Type::T_INT), 'currency' => array('title' => 'Currency', 'operatorType' => CRM_Report_Form::OP_MULTISELECT, 'options' => CRM_Core_OptionGroup::values('currencies_enabled'), 'default' => NULL, 'type' => CRM_Utils_Type::T_STRING), 'financial_type_id' => array('title' => ts('Financial Type'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, 'options' => self::$financial_types, 'type' => CRM_Utils_Type::T_INT), 'frequency_unit' => array('title' => ts('Frequency Unit'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, 'options' => CRM_Core_OptionGroup::values('recur_frequency_units')), self::$nscd_fid => array('title' => ts('Next Scheduled Contribution Date'), 'operatorType' => CRM_Report_Form::OP_DATE, 'type' => CRM_Utils_Type::T_DATE), 'next_scheduled_day' => array('title' => ts('Next Scheduled Day'), 'operatorType' => CRM_Report_Form::OP_INT, 'type' => CRM_Utils_Type::T_INT), 'cycle_day' => array('title' => ts('Cycle Day'), 'operatorType' => CRM_Report_Form::OP_INT, 'type' => CRM_Utils_Type::T_INT), 'start_date' => array('title' => ts('Start Date'), 'operatorType' => CRM_Report_Form::OP_DATE, 'type' => CRM_Utils_Type::T_DATE), 'modified_date' => array('title' => ts('Modified Date'), 'operatorType' => CRM_Report_Form::OP_DATE, 'type' => CRM_Utils_Type::T_DATE), 'cancel_date' => array('title' => ts('Cancel Date'), 'operatorType' => CRM_Report_Form::OP_DATE, 'type' => CRM_Utils_Type::T_DATE))), 'civicrm_address' => array('dao' => 'CRM_Core_DAO_Address', 'fields' => array('street_address' => array('title' => ts('Address'), 'default' => FALSE), 'supplemental_address_1' => array('title' => ts('Supplementary Address Field 1'), 'default' => FALSE), 'supplemental_address_2' => array('title' => ts('Supplementary Address Field 2'), 'default' => FALSE), 'city' => array('title' => 'City', 'default' => FALSE), 'state_province_id' => array('title' => 'Province', 'default' => FALSE, 'alter_display' => 'alterStateProvinceID'), 'postal_code' => array('title' => 'Postal Code', 'default' => FALSE), 'country_id' => array('title' => 'Country', 'default' => FALSE, 'alter_display' => 'alterCountryID')), 'grouping' => 'contact-fields')); if (empty(self::$financial_types)) { unset($this->_columns['civicrm_contribution_recur']['filters']['financial_type_id']); } parent::__construct(); }
function doDirectPayment(&$params) { if (!$this->_profile) { return self::error('Unexpected error, missing profile'); } // use the iATSService object for interacting with iATS, mostly the same for recurring contributions require_once "CRM/iATS/iATSService.php"; // TODO: force bail if it's not recurring? $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID']; $method = $isRecur ? 'acheft_create_customer_code' : 'acheft'; // to add debugging info in the drupal log, assign 1 to log['all'] below $iats = new iATS_Service_Request(array('type' => 'process', 'method' => $method, 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID'])); $request = $this->convertParams($params, $method); $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']; $credentials = array('agentCode' => $this->_paymentProcessor['user_name'], 'password' => $this->_paymentProcessor['password']); // Get the API endpoint URL for the method's transaction mode. // TODO: enable override of the default url in the request object // $url = $this->_paymentProcessor['url_site']; // make the soap request $response = $iats->request($credentials, $request); // process the soap response into a readable result $result = $iats->result($response); if ($result['status']) { $params['contribution_status_id'] = 2; // always pending status $params['payment_status_id'] = 2; // for future versions, the proper key $params['trxn_id'] = trim($result['remote_id']) . ':' . time(); $params['gross_amount'] = $params['amount']; if ($isRecur) { // save the client info in my custom table // Allow further manipulation of the arguments via custom hooks, // before initiating processCreditCard() // CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $iatslink1); $processresult = $response->PROCESSRESULT; $customer_code = (string) $processresult->CUSTOMERCODE; // $exp = sprintf('%02d%02d', ($params['year'] % 100), $params['month']); $exp = '0000'; $email = ''; if (isset($params['email'])) { $email = $params['email']; } elseif (isset($params['email-5'])) { $email = $params['email-5']; } elseif (isset($params['email-Primary'])) { $email = $params['email-Primary']; } $query_params = array(1 => array($customer_code, 'String'), 2 => array($request['customerIPAddress'], 'String'), 3 => array($exp, 'String'), 4 => array($params['contactID'], 'Integer'), 5 => array($email, 'String'), 6 => array($params['contributionRecurID'], 'Integer')); CRM_Core_DAO::executeQuery("INSERT INTO civicrm_iats_customer_codes\n (customer_code, ip, expiry, cid, email, recur_id) VALUES (%1, %2, %3, %4, %5, %6)", $query_params); // also set next_sched_contribution, the field name is civicrm version dependent $field_name = _iats_civicrm_nscd_fid(); $params[$field_name] = strtotime('+' . $params['frequency_interval'] . ' ' . $params['frequency_unit']); } return $params; } else { return self::error($result['reasonMessage']); } }
/** * hook_civicrm_pre * * Handle special cases of creating contribution (regular and recurring) records when using IATS Payments * * 1. CiviCRM assumes all recurring contributions need to be confirmed using the IPN mechanism. This is not true for iATS recurring contributions. * So when creating a contribution that is part of a recurring series, test for status = 2, and set to status = 1 instead. * Do this only for the initial contribution record. * The (subsequent) recurring contributions' status id is set explicitly in the job that creates it, this modification breaks that process. * * 2. For ACH/EFT, we also have the opposite problem - all contributions will need to verified by iATS and only later set to status success or * failed via the acheft verify job. We also want to modify the payment instrument from CC to ACH/EFT * * TODO: update this code with constants for the various id values of 1 and 2. * TODO: CiviCRM should have nicer ways to handle this. */ function iats_civicrm_pre($op, $objectName, $objectId, &$params) { // since this function gets called a lot, quickly determine if I care about the record being created if ('create' == $op && ('Contribution' == $objectName || 'ContributionRecur' == $objectName) && !empty($params['contribution_status_id'])) { // watchdog('iats_civicrm','hook_civicrm_pre for Contribution <pre>@params</pre>',array('@params' => print_r($params)); // figure out the payment processor id, not nice $version = CRM_Utils_System::version(); $payment_processor_id = 'ContributionRecur' == $objectName ? $params['payment_processor_id'] : (!empty($params['payment_processor']) ? $params['payment_processor'] : (!empty($params['contribution_recur_id']) ? _iats_civicrm_get_payment_processor_id($params['contribution_recur_id']) : 0)); if ($type = _iats_civicrm_is_iats($payment_processor_id)) { switch ($type . $objectName) { case 'iATSServiceContribution': // cc contribution, test if it's been set to status 2 on a recurring contribution // cc contribution, test if it's been set to status 2 on a recurring contribution case 'iATSServiceSWIPEContribution': // for civi version before 4.6.6, we had to force the status to 1 if (2 == $params['contribution_status_id'] && !empty($params['contribution_recur_id']) && version_compare($version, '4.6.6') < 0) { // but only for the first one $count = civicrm_api('Contribution', 'getcount', array('version' => 3, 'contribution_recur_id' => $params['contribution_recur_id'])); if (is_array($count) && empty($count['result']) || empty($count)) { // watchdog('iats_civicrm','hook_civicrm_pre updating status_id for objectName @id, count <pre>!count</pre>, params <pre>!params</pre>, ',array('@id' => $objectName, '!count' => print_r($count,TRUE),'!params' => print_r($params,TRUE))); $params['contribution_status_id'] = 1; } } break; case 'iATSServiceContributionRecur': // cc/swipe/ACHEFT recurring contribution record // cc/swipe/ACHEFT recurring contribution record case 'iATSServiceSWIPEContributionRecur': case 'iATSServiceACHEFTContributionRecur': // the next scheduled contribution date field name is civicrm version dependent $field_name = _iats_civicrm_nscd_fid(); // when creating a recurring contribution record via a civicrm contribution form // we've already taken the first payment, so calculate the next one (core assumes the intial contribution is pending) // we set this to 'in-progress' even for ACH/EFT if the first one hasn't been verified, because we still want to be attempting later ones // this condition helps avoid mangling records being imported from a csv file if (5 != $params['contribution_status_id'] && empty($params[$field_name])) { $params['contribution_status_id'] = 5; $params['trxn_id'] = NULL; $next = strtotime('+' . $params['frequency_interval'] . ' ' . $params['frequency_unit']); $params[$field_name] = date('YmdHis', $next); } if ($type == 'iATSServiceACHEFT') { // fix the payment type for ACH/EFT $params['payment_instrument_id'] = 2; } break; case 'iATSServiceACHEFTContribution': // ach/eft contribution: update the payment instrument $params['payment_instrument_id'] = 2; // and push the status to 2 if civicrm thinks it's 1, i.e. for one-time contributions // in other words, never create ach/eft contributions as complete, always push back to pending and verify if ($params['contribution_status_id'] == 1) { $params['contribution_status_id'] = 2; } break; case 'iATSServiceUKDDContribution': // UK DD contribution: update the payment instrument, fix the receive date $params['payment_instrument_id'] = 2; if ($start_date = strtotime($_POST['payer_validate_start_date'])) { $params['receive_date'] = date('Ymd', $start_date) . '120000'; } break; case 'iATSServiceUKDDContributionRecur': // UK DD recurring contribution record: update the payment instrument, fix the start_date $params['payment_instrument_id'] = 2; if ($start_date = strtotime($_POST['payer_validate_start_date'])) { $params['start_date'] = date('Ymd', $start_date) . '120000'; } break; } } // watchdog('iats_civicrm','ignoring hook_civicrm_pre for objectName @id',array('@id' => $objectName)); } }
/** * hook_civicrm_pre * * Handle special cases of creating contribution (regular and recurring) records when using IATS Payments * * 1. CiviCRM assumes all recurring contributions need to be confirmed using the IPN mechanism. This is not true for iATS recurring contributions. * So when creating a contribution that is part of a recurring series, test for status = 2, and set to status = 1 instead, unless we're using the fixed day feature * Do this only for the initial contribution record. * The (subsequent) recurring contributions' status id is set explicitly in the job that creates it, this modification breaks that process. * * 2. For ACH/EFT, we also have the opposite problem - all contributions will need to verified by iATS and only later set to status success or * failed via the acheft verify job. We also want to modify the payment instrument from CC to ACH/EFT * * TODO: update this code with constants for the various id values of 1 and 2. * TODO: CiviCRM should have nicer ways to handle this. */ function iats_civicrm_pre($op, $objectName, $objectId, &$params) { // since this function gets called a lot, quickly determine if I care about the record being created if ('create' == $op && ('Contribution' == $objectName || 'ContributionRecur' == $objectName) && !empty($params['contribution_status_id'])) { // watchdog('iats_civicrm','hook_civicrm_pre for Contribution <pre>@params</pre>',array('@params' => print_r($params)); // figure out the payment processor id, not nice $version = CRM_Utils_System::version(); $payment_processor_id = 'ContributionRecur' == $objectName ? $params['payment_processor_id'] : (!empty($params['payment_processor']) ? $params['payment_processor'] : (!empty($params['contribution_recur_id']) ? _iats_civicrm_get_payment_processor_id($params['contribution_recur_id']) : 0)); if ($type = _iats_civicrm_is_iats($payment_processor_id)) { $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings'); $allow_days = empty($settings['days']) ? array('-1') : $settings['days']; switch ($type . $objectName) { case 'iATSServiceContribution': // cc contribution, test if it's been set to status 2 on a recurring contribution // cc contribution, test if it's been set to status 2 on a recurring contribution case 'iATSServiceSWIPEContribution': // for civi version before 4.6.6, we had to force the status to 1 if (2 == $params['contribution_status_id'] && !empty($params['contribution_recur_id']) && max($allow_days) <= 0 && version_compare($version, '4.6.6') < 0) { // but only for the first one $count = civicrm_api('Contribution', 'getcount', array('version' => 3, 'contribution_recur_id' => $params['contribution_recur_id'])); if (is_array($count) && empty($count['result']) || empty($count)) { // watchdog('iats_civicrm','hook_civicrm_pre updating status_id for objectName @id, count <pre>!count</pre>, params <pre>!params</pre>, ',array('@id' => $objectName, '!count' => print_r($count,TRUE),'!params' => print_r($params,TRUE))); $params['contribution_status_id'] = 1; } } break; case 'iATSServiceContributionRecur': // cc/swipe/ACHEFT recurring contribution record // cc/swipe/ACHEFT recurring contribution record case 'iATSServiceSWIPEContributionRecur': case 'iATSServiceACHEFTContributionRecur': // the next scheduled contribution date field name is civicrm version dependent $field_name = _iats_civicrm_nscd_fid(); // when creating a recurring contribution record via a civicrm contribution form // we've already taken the first payment, so calculate the next one (core assumes the intial contribution is pending) // we set this to 'in-progress' even for ACH/EFT if the first one hasn't been verified, because we still want to be attempting later ones // this condition helps avoid mangling records being imported from a csv file if (5 != $params['contribution_status_id'] && empty($params[$field_name])) { $params['contribution_status_id'] = 5; $params['trxn_id'] = NULL; // civi wants to put the returned trxn_id in here $next = strtotime('+' . $params['frequency_interval'] . ' ' . $params['frequency_unit']); $params[$field_name] = date('YmdHis', $next); } if ($type == 'iATSServiceACHEFT') { // fix the payment type for ACH/EFT $params['payment_instrument_id'] = 2; } break; case 'iATSServiceACHEFTContribution': // ach/eft contribution: update the payment instrument $params['payment_instrument_id'] = 2; // and push the status to 2 if civicrm thinks it's 1, i.e. for one-time contributions // in other words, never create ach/eft contributions as complete, always push back to pending and verify if ($params['contribution_status_id'] == 1) { $params['contribution_status_id'] = 2; } break; case 'iATSServiceUKDDContribution': // UK DD contribution: update the payment instrument, fix the receive date $params['payment_instrument_id'] = 2; if ($start_date = strtotime($_POST['payer_validate_start_date'])) { $params['receive_date'] = date('Ymd', $start_date) . '120000'; } break; case 'iATSServiceUKDDContributionRecur': // UK DD recurring contribution record: update the payment instrument, fix the start_date $params['payment_instrument_id'] = 2; if ($start_date = strtotime($_POST['payer_validate_start_date'])) { $params['start_date'] = date('Ymd', $start_date) . '120000'; } break; } if ($type != 'iATSServiceUKDD' && $objectName == 'Contribution') { // new, non-UKDD contribution records in a schedule are forced to comply with any restrictions if (0 < max($allow_days)) { $from_time = _iats_contributionrecur_next(strtotime($params['receive_date']), $allow_days); $params['receive_date'] = date('Ymd', $from_time) . '030000'; } } } // watchdog('iats_civicrm','ignoring hook_civicrm_pre for objectName @id',array('@id' => $objectName)); } // if I've set fixed monthly recurring dates, force any iats (non uk dd) recurring contribution schedule records to comply // it's a bit draconian, and you likely want to give administrators the ability to modify these schedules // this is separate from the above because I want to deal with both create and edit possibilities if ('ContributionRecur' == $objectName && ('create' == $op || 'edit' == $op)) { if ($type = _iats_civicrm_is_iats($params['payment_processor_id'])) { if ($type != 'iATSServiceUKDD' && !empty($params['next_sched_contribution_date'])) { $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings'); $allow_days = empty($settings['days']) ? array('-1') : $settings['days']; if (0 < max($allow_days)) { // force one of the fixed days, and set the cycle_day at the same time $init_time = 'create' == $op ? time() : strtotime($params['next_sched_contribution_date']); $from_time = _iats_contributionrecur_next($init_time, $allow_days); $params['next_sched_contribution_date'] = date('YmdHis', $from_time); $params['cycle_day'] = date('j', $from_time); // day of month without leading 0 } } if (empty($params['installments'])) { // fix a civi bug while I'm here $params['installments'] = '0'; } } } }
/** * 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.')); }