protected function updateCreditCardCustomer($params)
 {
     require_once "CRM/iATS/iATSService.php";
     $credentials = iATS_Service_Request::credentials($params['paymentProcessorId'], $params['is_test']);
     unset($params['paymentProcessorId']);
     unset($params['is_test']);
     unset($params['domain']);
     $iats_service_params = array('type' => 'customer', 'iats_domain' => $credentials['domain'], 'method' => 'update_credit_card_customer');
     $iats = new iATS_Service_Request($iats_service_params);
     // print_r($iats); die();
     $params['updateCreditCardNum'] = 0 < strlen($params['creditCardNum']) && FALSE === strpos($params['creditCardNum'], '*') ? 1 : 0;
     if (empty($params['updateCreditCardNum'])) {
         unset($params['creditCardNum']);
         unset($params['updateCreditCardNum']);
     }
     $params['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
     foreach (array('qfKey', 'entryURL', 'firstName', 'lastName', '_qf_default', '_qf_IATSCustomerLink_submit') as $key) {
         if (isset($params[$key])) {
             unset($params[$key]);
         }
     }
     // make the soap request
     $response = $iats->request($credentials, $params);
     $result = $iats->result($response, TRUE);
     // note: don't log this to the iats_response table
     return $result;
 }
 function run()
 {
     // TODO: use the cid value to put the customer name in the title?
     // CRM_Utils_System::setTitle(ts('iATS CustomerLink'));
     $customerCode = CRM_Utils_Request::retrieve('customerCode', 'String');
     $paymentProcessorId = CRM_Utils_Request::retrieve('paymentProcessorId', 'Positive');
     $is_test = CRM_Utils_Request::retrieve('is_test', 'Integer');
     $this->assign('customerCode', $customerCode);
     require_once "CRM/iATS/iATSService.php";
     $credentials = iATS_Service_Request::credentials($paymentProcessorId, $is_test);
     $iats_service_params = array('type' => 'customer', 'iats_domain' => $credentials['domain'], 'method' => 'get_customer_code_detail');
     $iats = new iATS_Service_Request($iats_service_params);
     // print_r($iats); die();
     $request = array('customerCode' => $customerCode);
     // make the soap request
     $response = $iats->request($credentials, $request);
     $customer = $iats->result($response, FALSE);
     // note: don't log this to the iats_response table
     $ac1 = $customer['ac1'];
     // this is a SimpleXMLElement Object
     $attributes = $ac1->attributes();
     $type = $attributes['type'];
     $card = get_object_vars($ac1->{$type});
     $card['type'] = $type;
     foreach (array('ac1', 'status', 'remote_id', 'auth_result') as $key) {
         if (isset($customer[$key])) {
             unset($customer[$key]);
         }
     }
     $this->assign('customer', $customer);
     $this->assign('card', $card);
     parent::run();
 }
 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']);
     }
 }
 function run()
 {
     // generate json output from iats service calls
     $request = $_POST;
     $pp_id = (int) $request['payment_processor_id'];
     if (empty($pp_id)) {
         return;
     }
     $params = array('version' => 3, 'sequential' => 1, 'id' => $pp_id, 'return' => 'user_name');
     $result = civicrm_api('PaymentProcessor', 'getvalue', $params);
     $request['agentCode'] = $result;
     $params = array('version' => 3, 'sequential' => 1, 'id' => $pp_id, 'return' => 'url_site');
     $result = civicrm_api('PaymentProcessor', 'getvalue', $params);
     $request['iats_domain'] = parse_url($result, PHP_URL_HOST);
     foreach (array('reset', 'q', 'IDS_request_uri', 'IDS_user_agent', 'payment_processor_id') as $key) {
         if (isset($request[$key])) {
             unset($request[$key]);
         }
     }
     $options = array();
     foreach (array('type', 'method', 'iats_domain') as $key) {
         if (isset($request[$key])) {
             $options[$key] = $request[$key];
             unset($request[$key]);
         }
     }
     $credentials = array();
     foreach (array('agentCode', 'password') as $key) {
         if (isset($request[$key])) {
             $credentials[$key] = $request[$key];
             unset($request[$key]);
         }
     }
     // TODO: bail here if I don't have enough for my service request
     // use the iATSService object for interacting with iATS
     require_once "CRM/iATS/iATSService.php";
     $iats = new iATS_Service_Request($options);
     $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
     // make the soap request
     $response = $iats->request($credentials, $request);
     // process the soap response into a readable result
     if (!empty($response)) {
         $result = $iats->result($response);
     } else {
         $result = array('Invalid request');
     }
     // TODO: fix header
     // header('Content-Type: text/javascript');
     echo json_encode(array_merge($result));
     exit;
 }
 protected function getCustomerCodeDetail($params)
 {
     require_once "CRM/iATS/iATSService.php";
     $credentials = iATS_Service_Request::credentials($params['paymentProcessorId'], $params['is_test']);
     $iats_service_params = array('type' => 'customer', 'iats_domain' => $credentials['domain'], 'method' => 'get_customer_code_detail');
     $iats = new iATS_Service_Request($iats_service_params);
     // print_r($iats); die();
     $request = array('customerCode' => $params['customerCode']);
     // make the soap request
     $response = $iats->request($credentials, $request);
     $customer = $iats->result($response, FALSE);
     // note: don't log this to the iats_response table
     // print_r($customer); die();
     $ac1 = $customer['ac1'];
     // this is a SimpleXMLElement Object
     $card = get_object_vars($ac1->CC);
     return $customer + $card;
 }
function _iats_process_contribution_payment($contribution, $options)
{
    // first create the pending contribution, and save its id
    $contributionResult = civicrm_api('contribution', 'create', $contribution);
    $contribution_id = CRM_Utils_Array::value('id', $contributionResult);
    // connect to a membership if requested
    if (!empty($options['membership_id'])) {
        try {
            civicrm_api3('MembershipPayment', 'create', array('contribution_id' => $contribution_id, 'membership_id' => $options['membership_id']));
        } catch (Exception $e) {
            // ignore
        }
    }
    // now try to get the money, and then do one of: update the contribution to failed, complete the transaction, or update a pending ach/eft with it's transaction id
    require_once "CRM/iATS/iATSService.php";
    switch ($options['subtype']) {
        case 'ACHEFT':
            $method = 'acheft_with_customer_code';
            $contribution_status_id = 2;
            // will not complete
            break;
        default:
            $method = 'cc_with_customer_code';
            $contribution_status_id = 1;
            break;
    }
    $credentials = iATS_Service_Request::credentials($contribution['payment_processor'], $contribution['is_test']);
    $iats_service_params = array('method' => $method, 'type' => 'process', 'iats_domain' => $credentials['domain']);
    $iats = new iATS_Service_Request($iats_service_params);
    // build the request array
    $request = array('customerCode' => $options['customer_code'], 'invoiceNum' => $contribution['invoice_id'], 'total' => $contribution['total_amount']);
    $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
    // make the soap request
    $response = $iats->request($credentials, $request);
    // process the soap response into a readable result
    $result = $iats->result($response);
    if (empty($result['status'])) {
        /* update the contribution record in civicrm  */
        /* with the failed transaction status or pending if I had a server issue */
        /* and include the reason in the source field */
        $contribution_status_id = empty($result['auth_result']) ? 2 : 4;
        $contribution = array('id' => $contribution_id, 'source' => $contribution['source'] . ' ' . $result['reasonMessage'], 'contribution_status_id' => $contribution_status_id);
        $contributionResult = civicrm_api3('contribution', 'create', $contribution);
        return ts('Failed to process recurring contribution id %1: ', array(1 => $contribution['contribution_recur_id'])) . $result['reasonMessage'];
    } elseif ($contribution_status_id == 1) {
        /* success, done */
        $trxn_id = trim($result['remote_id']) . ':' . time();
        $complete = array('id' => $contribution_id, 'trxn_id' => $trxn_id, 'receive_date' => $receive_date);
        $complete['is_email_receipt'] = empty($options['is_email_receipt']) ? 0 : 1;
        try {
            $contributionResult = civicrm_api3('contribution', 'completetransaction', $complete);
        } catch (Exception $e) {
            throw new API_Exception('Failed to complete transaction: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
        }
        // restore my source field that ipn irritatingly overwrites, and make sure that the trxn_id is set also
        civicrm_api3('contribution', 'setvalue', array('id' => $contribution_id, 'value' => $contribution['source'], 'field' => 'source'));
        civicrm_api3('contribution', 'setvalue', array('id' => $contribution_id, 'value' => $trxn_id, 'field' => 'trxn_id'));
        return ts('Successfully processed recurring contribution id %1: ', array(1 => $contribution['contribution_recur_id'])) . $result['auth_result'];
    } else {
        // success, but just update the transaction id, wait for completion
        $contribution = array('id' => $contribution_id, 'trxn_id' => trim($result['remote_id']) . ':' . time());
        $contributionResult = civicrm_api3('contribution', 'create', $contribution);
        return ts('Successfully processed pending recurring contribution id %1: ', array(1 => $contribution_recur_id)) . $result['auth_result'];
    }
}
 function doDirectPayment(&$params)
 {
     $error = $this->checkParams($params);
     if (!empty($error)) {
         return $error;
     }
     // $params['start_date'] = $params['receive_date'];
     // use the iATSService object for interacting with iATS
     require_once "CRM/iATS/iATSService.php";
     $iats = new iATS_Service_Request(array('type' => 'customer', 'method' => 'direct_debit_create_acheft_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
     $schedule = $this->getSchedule($params);
     if (!is_array($schedule)) {
         // assume an error object to return
         return $schedule;
     }
     $request = array_merge($this->convertParamsCreateCustomerCode($params), $schedule);
     $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
     $request['customerCode'] = '';
     $request['accountType'] = 'CHECKING';
     $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);
     // drupal_set_message('<pre>'.print_r($result,TRUE).'</pre>');
     if ($result['status']) {
         $params['contribution_status_id'] = 2;
         // always pending
         $params['payment_status_id'] = 2;
         // for future versions, the proper key
         $params['trxn_id'] = trim($result['remote_id']) . ':' . time();
         $params['gross_amount'] = $params['amount'];
         // save the client info in my custom table
         // Allow further manipulation of the arguments via custom hooks,
         $customer_code = $result['CUSTOMERCODE'];
         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('', 'String'), 4 => array($params['contactID'], 'Integer'), 5 => array($email, 'String'), 6 => array($params['contributionRecurID'], 'Integer'));
         // drupal_set_message('<pre>'.print_r($query_params,TRUE).'</pre>');
         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);
         // save their payer validation data in civicrm_iats_ukdd_validate
         $query_params = array(1 => array($customer_code, 'String'), 2 => array($params['payer_validate_reference'], 'String'), 3 => array($params['contactID'], 'Integer'), 4 => array($params['contributionRecurID'], 'Integer'), 5 => array($params['payer_validate_declaration'], 'Integer'), 6 => array(date('c'), 'String'));
         // drupal_set_message('<pre>'.print_r($query_params,TRUE).'</pre>');
         CRM_Core_DAO::executeQuery("INSERT INTO civicrm_iats_ukdd_validate\n        (customer_code, acheft_reference_num, cid, recur_id, validated, validated_datetime) VALUES (%1, %2, %3, %4, %5, %6)", $query_params);
         // set the status of the initial contribution to pending (currently is redundant), and the date to what I'm asking iATS for
         $params['contribution_status_id'] = 2;
         $params['start_date'] = $params['payer_validate_start_date'];
         // optimistically set this date, even though CiviCRM will likely not do anything with it yet - I'll change it with my pre hook in the meanwhile
         $params['receive_date'] = $params['payer_validate_start_date'];
         // also set next_sched_contribution, though it won't be used
         $params['next_sched_contribution'] = strtotime($params['payer_validate_start_date'] . ' + ' . $params['frequency_interval'] . ' ' . $params['frequency_unit']);
         return $params;
     } else {
         return self::error($result['reasonMessage']);
     }
 }
 function doDirectPayment(&$params)
 {
     if (!$this->_profile) {
         return self::error('Unexpected error, missing profile');
     }
     // use the iATSService object for interacting with iATS. Recurring contributions go through a more complex process.
     require_once "CRM/iATS/iATSService.php";
     $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'];
     $methodType = $isRecur ? 'customer' : 'process';
     $method = $isRecur ? 'create_credit_card_customer' : 'cc';
     $iats = new iATS_Service_Request(array('type' => $methodType, '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);
     if (!$isRecur) {
         // process the soap response into a readable result, logging any credit card transactions
         $result = $iats->result($response);
         if ($result['status']) {
             $params['contribution_status_id'] = 1;
             // success
             $params['payment_status_id'] = 1;
             // for versions >= 4.6.6, the proper key
             $params['trxn_id'] = trim($result['remote_id']) . ':' . time();
             $params['gross_amount'] = $params['amount'];
             return $params;
         } else {
             return self::error($result['reasonMessage']);
         }
     } else {
         // save the client info in my custom table, then (maybe) run the transaction
         $customer = $iats->result($response, FALSE);
         // print_r($customer);
         if ($customer['status']) {
             $processresult = $response->PROCESSRESULT;
             $customer_code = (string) $processresult->CUSTOMERCODE;
             $exp = sprintf('%02d%02d', $params['year'] % 100, $params['month']);
             $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);
             $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings');
             $allow_days = empty($settings['days']) ? array('-1') : $settings['days'];
             if (max($allow_days) <= 0) {
                 // run the transaction immediately
                 $iats = new iATS_Service_Request(array('type' => 'process', 'method' => 'cc_with_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
                 $request = array('invoiceNum' => $params['invoiceID']);
                 $request['total'] = sprintf('%01.2f', CRM_Utils_Rule::cleanMoney($params['amount']));
                 $request['customerCode'] = $customer_code;
                 $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
                 $response = $iats->request($credentials, $request);
                 $result = $iats->result($response);
                 if ($result['status']) {
                     $params['contribution_status_id'] = 1;
                     // success
                     $params['payment_status_id'] = 1;
                     // for versions >= 4.6.6, the proper key
                     $params['trxn_id'] = trim($result['remote_id']) . ':' . time();
                     $params['gross_amount'] = $params['amount'];
                     $params['next_sched_contribution'] = strtotime('+' . $params['frequency_interval'] . ' ' . $params['frequency_unit']);
                     return $params;
                 } else {
                     return self::error($result['reasonMessage']);
                 }
             } else {
                 // I've got a schedule to adhere to!
                 $params['contribution_status_id'] = 2;
                 // pending
                 $params['payment_status_id'] = 2;
                 // for versions >= 4.6.6, the proper key
                 $from_time = _iats_contributionrecur_next(time(), $allow_days);
                 $params['next_sched_contribution'] = $params['receive_date'] = date('Ymd', $from_time) . '030000';
                 return $params;
             }
             return self::error('Unexpected error');
         } else {
             return self::error($customer['reasonMessage']);
         }
     }
 }
/**
 * 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.'));
    }
    // 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 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');
    }
    $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()) {
        // 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_date = date("YmdHis");
        // i.e. now
        // 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 {
            // so far so, good ... create the pending contribution, and save its id
            $contributionResult = civicrm_api('contribution', 'create', $contribution);
            $contribution_id = CRM_Utils_Array::value('id', $contributionResult);
            // if our template contribution has a membership payment, make this one also
            if (!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'])) {
                        civicrm_api('MembershipPayment', 'create', array('version' => 3, 'contribution_id' => $contribution_id, 'membership_id' => $membership_payment['membership_id']));
                    }
                } catch (Exception $e) {
                    // ignore, if will fail correctly if there is no membership payment
                }
            }
            // now try to get the money, and then do one of: update the contribution to failed, complete the transaction, or update a pending ach/eft with it's transaction id
            require_once "CRM/iATS/iATSService.php";
            switch ($subtype) {
                case 'ACHEFT':
                    $method = 'acheft_with_customer_code';
                    $contribution_status_id = 2;
                    // will not complete
                    break;
                default:
                    $method = 'cc_with_customer_code';
                    $contribution_status_id = 1;
                    break;
            }
            $iats_service_params = array('method' => $method, 'type' => 'process', 'iats_domain' => parse_url($dao->url_site, PHP_URL_HOST));
            $iats = new iATS_Service_Request($iats_service_params);
            // build the request array
            $request = array('customerCode' => $dao->customer_code, 'invoiceNum' => $hash, 'total' => $total_amount);
            $request['customerIPAddress'] = function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR'];
            $credentials = iATS_Service_Request::credentials($dao->payment_processor_id, $contribution['is_test']);
            // make the soap request
            $response = $iats->request($credentials, $request);
            // process the soap response into a readable result
            $result = $iats->result($response);
            if (empty($result['status'])) {
                /* update the contribution record in civicrm with the failed status and include the reason in the source field */
                $contribution = array('version' => 3, 'id' => $contribution_id, 'source' => $contribution['source'] . ' ' . $result['reasonMessage'], 'contribution_status_id' => 4);
                $contributionResult = civicrm_api('contribution', 'create', $contribution);
                $output[] = ts('Failed to process recurring contribution id %1: ', array(1 => $contribution_recur_id)) . $result['reasonMessage'];
            } elseif ($contribution_status_id == 1) {
                /* success, done */
                $trxn_id = trim($result['remote_id']) . ':' . time();
                $complete = array('version' => 3, 'id' => $contribution_id, 'trxn_id' => $trxn_id, 'receive_date' => $receive_date);
                $complete['is_email_receipt'] = $receipt_recurring;
                /* do not send receipt by default. TODO: make it configurable */
                try {
                    $contributionResult = civicrm_api('contribution', 'completetransaction', $complete);
                    // restore my source field that ipn irritatingly overwrites, and make sure that the trxn_id is set also
                    civicrm_api('contribution', 'setvalue', array('version' => 3, 'id' => $contribution_id, 'value' => $source, 'field' => 'source'));
                    civicrm_api('contribution', 'setvalue', array('version' => 3, 'id' => $contribution_id, 'value' => $trxn_id, 'field' => 'trxn_id'));
                } catch (Exception $e) {
                    throw new API_Exception('Failed to complete transaction: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
                }
                $output[] = ts('Successfully processed recurring contribution id %1: ', array(1 => $contribution_recur_id)) . $result['auth_result'];
            } else {
                // success, but just update the transaction id, wait for completion
                $contribution = array('version' => 3, 'id' => $contribution_id, 'trxn_id' => trim($result['remote_id']) . ':' . time());
                $contributionResult = civicrm_api('contribution', 'create', $contribution);
                $output[] = ts('Successfully processed pending recurring contribution id %1: ', array(1 => $contribution_recur_id)) . $result['auth_result'];
            }
        }
        //$mem_end_date = $member_dao->end_date;
        // $temp_date = strtotime($dao->next_sched_contribution);
        /* calculate the next collection date. You could use the previous line instead if you wanted to catch up with missing contributions instead of just moving forward from the present */
        $temp_date = time();
        $next_collectionDate = strtotime("+{$dao->frequency_interval} {$dao->frequency_unit}", $temp_date);
        $next_collectionDate = date('YmdHis', $next_collectionDate);
        CRM_Core_DAO::executeQuery("\n      UPDATE civicrm_contribution_recur \n         SET " . IATS_CIVICRM_NSCD_FID . " = %1 \n       WHERE id = %2\n    ", array(1 => array($next_collectionDate, 'String'), 2 => 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.'));
}