protected function processCreditCardCustomer($values) { // generate another recurring contribution, matching our recurring template with submitted value $total_amount = $values['amount']; $contribution_template = _iats_civicrm_getContributionTemplate(array('contribution_recur_id' => $values['crid'])); $contact_id = $values['cid']; $hash = md5(uniqid(rand(), true)); $contribution_recur_id = $values['crid']; $payment_processor_id = $values['paymentProcessorId']; $type = _iats_civicrm_is_iats($payment_processor_id); $subtype = substr($type, 11); $source = "iATS Payments {$subtype} Recurring Contribution (id={$contribution_recur_id})"; $receive_date = date("YmdHis", time()); // i.e. now $contribution = array('version' => 3, 'contact_id' => $contact_id, 'receive_date' => $receive_date, 'total_amount' => $total_amount, 'contribution_recur_id' => $contribution_recur_id, 'invoice_id' => $hash, 'source' => $source, 'contribution_status_id' => 2, 'payment_processor' => $payment_processor_id, 'is_test' => $values['is_test']); foreach (array('payment_instrument_id', 'currency', 'financial_type_id') as $key) { $contribution[$key] = $contribution_template[$key]; } $options = array('is_email_receipt' => 0, 'customer_code' => $values['customerCode'], 'subtype' => $subtype); // now all the hard work in this function, recycled from the original recurring payment job $result = _iats_process_contribution_payment($contribution, $options); return $result; }
/** * Job.IatsACHEFTVerify API * * @param array $params * @return array API result descriptor * @see civicrm_api3_create_success * @see civicrm_api3_create_error * @throws API_Exception * Look up all pending (status = 2) ACH/EFT contributions and see if they've been approved or rejected * Update the corresponding recurring contribution record to status = 1 (or 4) * This works for both the initial contribution and subsequent contributions of recurring contributions, as well as one offs. * TODO: what kind of alerts should be provided if it fails? * * Also lookup new UK direct debit series, and new contributions from existing series. */ function civicrm_api3_job_iatsacheftverify($iats_service_params) { $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings'); $receipt_recurring = empty($settings['receipt_recurring']) ? 0 : 1; define('IATS_VERIFY_DAYS', 30); // I've added an extra 2 days when getting candidates from CiviCRM to be sure i've got them all. $civicrm_verify_days = IATS_VERIFY_DAYS + 2; // get all the pending direct debit contributions that still need approval within the last civicrm_verify_days $select = 'SELECT id, trxn_id, invoice_id, contact_id, contribution_recur_id, receive_date FROM civicrm_contribution WHERE contribution_status_id = 2 AND payment_instrument_id = 2 AND receive_date > %1 AND is_test = 0'; $args = array(1 => array(date('c', strtotime('-' . $civicrm_verify_days . ' days')), 'String')); $dao = CRM_Core_DAO::executeQuery($select, $args); $acheft_pending = array(); while ($dao->fetch()) { /* we assume that the iATS transaction id is a unique field for matching, and that it is stored as the first part of the civicrm transaction */ /* this is not unreasonable, assuming that the site doesn't have other active direct debit payment processors with similar patterns */ $key = current(explode(':', $dao->trxn_id, 2)); $acheft_pending[$key] = array('id' => $dao->id, 'trxn_id' => $dao->trxn_id, 'invoice_id' => $dao->invoice_id, 'contact_id' => $dao->contact_id, 'contribution_recur_id' => $dao->contribution_recur_id, 'receive_date' => $dao->receive_date); } // and some recent UK DD recurring contributions $select = 'SELECT c.id, c.contribution_status_id, c.trxn_id, c.invoice_id, icc.customer_code FROM civicrm_contribution c INNER JOIN civicrm_contribution_recur cr ON c.contribution_recur_id = cr.id 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 c.receive_date > %1 AND pp.class_name = %2 AND pp.is_test = 0'; $args[2] = array('Payment_iATSServiceUKDD', 'String'); $dao = CRM_Core_DAO::executeQuery($select, $args); $ukdd_contribution = array(); while ($dao->fetch()) { if (empty($ukdd_contribution[$dao->customer_code])) { $ukdd_contribution[$dao->customer_code] = array(); } // I want to key on my trxn_id that I can match up with data from iATS, but use the invoice_id for that initial pending one $key = empty($dao->trxn_id) ? $dao->invoice_id : $dao->trxn_id; $ukdd_contribution[$dao->customer_code][$key] = array('id' => $dao->id, 'contribution_status_id' => $dao->contribution_status_id, 'invoice_id' => $dao->invoice_id); } // and now get all the non-completed UKDD sequences, in order to track new contributions from iATS $select = 'SELECT cr.*, icc.customer_code as customer_code, icc.cid as icc_contact_id, iukddv.acheft_reference_num as reference_num, 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 INNER JOIN civicrm_iats_ukdd_validate iukddv ON cr.id = iukddv.recur_id WHERE pp.class_name = %1 AND pp.is_test = 0 AND (cr.end_date IS NULL OR cr.end_date > NOW())'; $args = array(1 => array('Payment_iATSServiceUKDD', 'String')); $dao = CRM_Core_DAO::executeQuery($select, $args); $ukdd_contribution_recur = array(); while ($dao->fetch()) { $ukdd_contribution_recur[$dao->customer_code] = get_object_vars($dao); } /* get "recent" approvals and rejects from iats and match them up with my pending list, or one-offs, or UK DD via the customer code */ require_once "CRM/iATS/iATSService.php"; // an array of methods => contribution status of the records retrieved $process_methods = array('acheft_journal_csv' => 1, 'acheft_payment_box_journal_csv' => 1, 'acheft_payment_box_reject_csv' => 4); /* initialize some values so I can report at the end */ $error_count = 0; // count the number of each record from iats analysed, and the number of each kind found $processed = array_fill_keys(array_keys($process_methods), 0); $found = array('recur' => 0, 'quick' => 0, 'new' => 0); // save all my api result messages as well $output = array(); /* do this loop for each relevant payment processor of type ACHEFT or UKDD */ /* since test payments are NEVER verified by iATS, don't bother checking them [unless/until they change this?] */ $select = 'SELECT id,url_site,is_test FROM civicrm_payment_processor WHERE (class_name = %1 OR class_name = %2) AND is_test = 0'; $args = array(1 => array('Payment_iATSServiceACHEFT', 'String'), 2 => array('Payment_iATSServiceUKDD', 'String')); $dao = CRM_Core_DAO::executeQuery($select, $args); // watchdog('civicrm_iatspayments_com', 'pending: <pre>!pending</pre>', array('!pending' => print_r($iats_acheft_recur_pending,TRUE)), WATCHDOG_NOTICE); while ($dao->fetch()) { /* get approvals from yesterday, approvals from previous days, and then rejections for this payment processor */ $iats_service_params = array('type' => 'report', 'iats_domain' => parse_url($dao->url_site, PHP_URL_HOST)) + $iats_service_params; /* the is_test below should always be 0, but I'm leaving it in, in case eventually we want to be verifying tests */ $credentials = iATS_Service_Request::credentials($dao->id, $dao->is_test); foreach ($process_methods as $method => $contribution_status_id) { // TODO: this is set to capture approvals and cancellations from the past month, for testing purposes // it doesn't hurt, but on a live environment, this maybe should be limited to the past week, or less? // or, it could be configurable for the job $iats_service_params['method'] = $method; $iats = new iATS_Service_Request($iats_service_params); // I'm now using the new v2 version of the payment_box_journal, so a previous hack here is now removed switch ($method) { case 'acheft_journal_csv': // special case to get today's transactions, so we're as real-time as we can be $request = array('date' => date('Y-m-d') . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); break; default: // box journals only go up to the end of yesterday $request = array('fromDate' => date('Y-m-d', strtotime('-' . IATS_VERIFY_DAYS . ' days')) . 'T00:00:00+00:00', 'toDate' => date('Y-m-d', strtotime('-1 day')) . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); break; } // make the soap request, should return a csv file $response = $iats->request($credentials, $request); $transactions = $iats->getCSV($response, $method); if ($method == 'acheft_journal_csv') { // also grab yesterday + day before yesterday + day before that + the day before that if it (in case of stat holiday - long weekend) $request = array('date' => date('Y-m-d', strtotime('-1 day')) . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); $response = $iats->request($credentials, $request); $transactions = array_merge($transactions, $iats->getCSV($response, $method)); $request = array('date' => date('Y-m-d', strtotime('-2 days')) . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); $response = $iats->request($credentials, $request); $transactions = array_merge($transactions, $iats->getCSV($response, $method)); $request = array('date' => date('Y-m-d', strtotime('-3 days')) . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); $response = $iats->request($credentials, $request); $transactions = array_merge($transactions, $iats->getCSV($response, $method)); $request = array('date' => date('Y-m-d', strtotime('-4 days')) . 'T23:59:59+00:00', 'customerIPAddress' => function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']); $response = $iats->request($credentials, $request); $transactions = array_merge($transactions, $iats->getCSV($response, $method)); } $processed[$method] += count($transactions); // watchdog('civicrm_iatspayments_com', 'transactions: <pre>!trans</pre>', array('!trans' => print_r($transactions,TRUE)), WATCHDOG_NOTICE); foreach ($transactions as $transaction_id => $transaction) { $contribution = NULL; // use this later to trigger an activity if it's not NULL // first deal with acheft_pending, [and possibly the corresponding recur sequence ? no? ] if (!empty($acheft_pending[$transaction_id])) { /* update the contribution status */ /* todo: additional sanity testing? We're assuming the uniqueness of the iATS transaction id here */ $is_recur = 'quick client' != strtolower($transaction->customer_code); $found[$is_recur ? 'recur' : 'quick']++; $contribution = $acheft_pending[$transaction_id]; // updating a contribution status to complete needs some extra bookkeeping if (1 == $contribution_status_id) { // note that I'm updating the timestamp portion of the transaction id here, since this might be useful at some point // should I update the receive date to when it was actually received? Would that confuse membership dates? $trxn_id = $transaction_id . ':' . time(); $complete = array('version' => 3, 'id' => $contribution['id'], 'trxn_id' => $transaction_id . ':' . time(), 'receive_date' => $contribution['receive_date']); if ($is_recur) { $complete['is_email_receipt'] = $receipt_recurring; /* use my saved setting for recurring completions */ } 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('version' => 3, 'id' => $contribution['id'], 'value' => $contribution['source'], 'field' => 'source')); civicrm_api3('contribution', 'setvalue', array('version' => 3, 'id' => $contribution['id'], 'value' => $trxn_id, 'field' => 'trxn_id')); } else { $params = array('version' => 3, 'sequential' => 1, 'contribution_status_id' => $contribution_status_id, 'id' => $contribution['id']); $result = civicrm_api3('Contribution', 'create', $params); // update the contribution } // always log these requests in my cutom civicrm table for auditing type purposes // watchdog('civicrm_iatspayments_com', 'contribution: <pre>!contribution</pre>', array('!contribution' => print_r($query_params,TRUE)), WATCHDOG_NOTICE); $query_params = array(1 => array($transaction->customer_code, 'String'), 2 => array($contribution['contact_id'], 'Integer'), 3 => array($contribution['id'], 'Integer'), 4 => array($contribution_status_id, 'Integer'), 5 => array($contribution['contribution_recur_id'], 'Integer')); if (empty($contribution['contribution_recur_id'])) { unset($query_params[5]); CRM_Core_DAO::executeQuery("INSERT INTO civicrm_iats_verify\n (customer_code, cid, contribution_id, contribution_status_id, verify_datetime) VALUES (%1, %2, %3, %4, NOW())", $query_params); } else { CRM_Core_DAO::executeQuery("INSERT INTO civicrm_iats_verify\n (customer_code, cid, contribution_id, contribution_status_id, verify_datetime, recur_id) VALUES (%1, %2, %3, %4, NOW(), %5)", $query_params); } } elseif (isset($ukdd_contribution_recur[$transaction->customer_code])) { // it's a (possibly) new recurring UKDD contribution triggered from iATS // check my existing ukdd_contribution list in case it's the first one that just needs to be updated, or has already been processed // I also confirm that it's got the right ach reference field, which i get from the ukdd_contribution_recur record $contribution_recur = $ukdd_contribution_recur[$transaction->customer_code]; // build the (unique) civicrm trxn id that we can use to match up against civicrm-stored transactions $trxn_id = $transaction->id . ':iATSUKDD:' . $transaction->customer_code; // sanity check against the ACH Reference number, but only if I get it from iATS if (!empty($transaction->achref) && $contribution_recur['reference_num'] != $transaction->achref) { $output[] = ts('Unexpected error: ACH Ref. %1 does not match for customer code %2 (should be %3)', array(1 => $transaction->achref, 2 => $transaction->customer_code, 3 => $contribution_recur['reference_num'])); ++$error_count; } elseif (isset($ukdd_contribution[$transaction->customer_code][$trxn_id])) { // I can ignore it, i've already created this one } else { // save my contribution in civicrm $contribution = array('version' => 3, 'contact_id' => $contribution_recur['contact_id'], 'receive_date' => date('c', $transaction->receive_date), 'total_amount' => $transaction->amount, 'payment_instrument_id' => $contribution_recur['payment_instrument_id'], 'contribution_recur_id' => $contribution_recur['id'], 'trxn_id' => $trxn_id, 'invoice_id' => md5(uniqid(rand(), TRUE)), 'source' => 'iATS UK DD Reference: ' . $contribution_recur['reference_num'], 'contribution_status_id' => $contribution_status_id, 'currency' => $contribution_recur['currency'], 'payment_processor' => $contribution_recur['payment_processor_id'], 'is_test' => 0); if (isset($dao->contribution_type_id)) { // 4.2 $contribution['contribution_type_id'] = $contribution_recur['contribution_type_id']; } else { // 4.3+ $contribution['financial_type_id'] = $contribution_recur['financial_type_id']; } // if I have an outstanding pending contribution for this series, I'll recycle and update it here foreach ($ukdd_contribution[$transaction->customer_code] as $key => $contrib_ukdd) { if ($contrib_ukdd['contribution_status_id'] == 2) { // it's pending $contribution['id'] = $contrib_ukdd['id']; // don't change my invoice id in this case unset($contribution['invoice_id']); // ensure I don't pull this trick more than once somehow unset($ukdd_contribution[$transaction->customer_code][$key]); // and note that I ignore everything else about the pending contribution in civicrm break; } } // otherwise I'll make do with a template if available $contribution_template = array(); if (empty($contribution['id'])) { // populate my contribution from a template if possible $contribution_template = _iats_civicrm_getContributionTemplate(array('contribution_recur_id' => $contribution_recur['id'], 'total_amount' => $transation->amount)); $get_from_template = array('contribution_campaign_id', 'amount_level'); foreach ($get_from_template as $field) { if (isset($contribution_template[$field])) { $contribution[$field] = $contribution_template[$field]; } } if (!empty($contribution_template['line_items'])) { $contribution['skipLineItem'] = 1; $contribution['api.line_item.create'] = $contribution_template['line_items']; } } if ($contribution_status_id == 1) { // create or update as pending and then complete $contribution['contribution_status_id'] = 2; $result = civicrm_api('contribution', 'create', $contribution); $complete = array('version' => 3, 'id' => $result['id'], 'trxn_id' => $trxn_id, 'receive_date' => $contribution['receive_date']); $complete['is_email_receipt'] = $receipt_recurring; /* send according to my configuration */ 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' => $contribution['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()); } } else { // create or update $result = civicrm_api('contribution', 'create', $contribution); } if ($result['is_error']) { $output[] = $result['error_message']; } else { $found['new']++; } } } // if one of the above was true and I've got a new or confirmed contribution: // so log it as an activity for administrative reference if (!empty($contribution)) { $subject_string = empty($contribution['id']) ? 'Found new iATS Payments UK DD contribution for contact id %3' : '%1 iATS Payments ACH/EFT contribution id %2 for contact id %3'; $subject = ts($subject_string, array(1 => $contribution_status_id == 4 ? ts('Cancelled') : ts('Verified'), 2 => $contribution['id'], 3 => $contribution['contact_id'])); $result = civicrm_api('activity', 'create', array('version' => 3, 'activity_type_id' => 6, 'source_contact_id' => $contribution['contact_id'], 'assignee_contact_id' => $contribution['contact_id'], 'subject' => $subject, '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 => $contribution['contact_id'], 2 => $result['error_message'])); ++$error_count; } else { $output[] = $subject; } } // otherwise ignore it } } } $message = '<br />' . ts('Completed with %1 errors.', array(1 => $error_count)); $message .= '<br />' . ts('Processed %1 approvals from today and past 4 days, %2 approval and %3 rejection records from the previous ' . IATS_VERIFY_DAYS . ' days.', array(1 => $processed['acheft_journal_csv'], 2 => $processed['acheft_payment_box_journal_csv'], 3 => $processed['acheft_payment_box_reject_csv'])); // If errors .. if ($error_count) { return civicrm_api3_create_error($message . '</br />' . implode('<br />', $output)); } // If no errors and some records processed .. if (array_sum($processed) > 0) { if (count($acheft_pending) > 0) { $message .= '<br />' . ts('For %1 pending ACH/EFT contributions, %2 non-recuring and %3 recurring contribution results applied.', array(1 => count($acheft_pending), 2 => $found['quick'], 3 => $found['recur'])); } if (count($ukdd_contribution_recur) > 0) { $message .= '<br />' . ts('For %1 recurring UK direct debit contribution series, %2 new contributions found.', array(1 => count($ukdd_contribution_recur), 2 => $found['new'])); } return civicrm_api3_create_success($message . '<br />' . implode('<br />', $output)); } // No records processed return civicrm_api3_create_success(ts('No records found to process.')); }
/** * 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.')); }