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(); }
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 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; }
/** * 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.')); }
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 iats_civicrm_buildForm_Contribution_Frontend(&$form) { if (empty($form->_paymentProcessors)) { return; } $acheft = iats_civicrm_processors($form->_paymentProcessors, 'ACHEFT'); $swipe = iats_civicrm_processors($form->_paymentProcessors, 'SWIPE'); $ukdd = iats_civicrm_processors($form->_paymentProcessors, 'UKDD'); // include the required javascripts for available customized selections // TODO: skip this if we're just loading a fragment of the page via ajax // If a form allows ACH/EFT and enables recurring, set recurring to the default if (0 < count($acheft)) { CRM_Core_Resources::singleton()->addScriptFile('com.iatspayments.civicrm', 'js/dd_acheft.js', 10, 'html-header'); // country specific js needs to be loaded here so it's available later CRM_Core_Resources::singleton()->addScriptFile('com.iatspayments.civicrm', 'js/dd_cad.js', 10, 'html-header'); if (isset($form->_elementIndex['is_recur'])) { $form->setDefaults(array('is_recur' => 1)); // make recurring contrib default to true } } if (0 < count($swipe)) { CRM_Core_Resources::singleton()->addScriptFile('com.iatspayments.civicrm', 'js/swipe.js', 10, 'html-header'); } if (0 < count($ukdd)) { CRM_Core_Resources::singleton()->addScriptFile('com.iatspayments.civicrm', 'js/dd_uk.js', 10, 'html-header'); if (isset($form->_elementIndex['is_recur'])) { $form->setDefaults(array('is_recur' => 1)); // make recurring contrib default to true } } /* Mangle (in a currency-dependent way) the ajax-bit of the form if I've just selected an ach/eft option */ if (!empty($acheft[$form->_paymentProcessor['id']])) { iats_acheft_form_customize($form); // watchdog('iats_acheft',kprint_r($form,TRUE)); } /* now something similar for swipe */ if (!empty($swipe[$form->_paymentProcessor['id']]) && !empty($form->_elementIndex['credit_card_exp_date'])) { iats_swipe_form_customize($form); } /* UK Direct debit option */ if (!empty($ukdd[$form->_paymentProcessor['id']])) { iats_ukdd_form_customize($form); // watchdog('iats_acheft',kprint_r($form,TRUE)); } /* and finally, for most frontend forms, use the dpm.js script to use the DirectPost Method override * TODO: provide an admin way to prevent this * TODO: use it for swipe, never for ukdd? */ require_once "CRM/iATS/iATSService.php"; if (iATS_Service_Request::isDPM($form->_paymentProcessor)) { CRM_Core_Region::instance('billing-block')->add(array('template' => 'CRM/iATS/BillingBlockDPM.tpl')); $iats_domain = parse_url($form->_paymentProcessor['url_site'], PHP_URL_HOST); $dpm_url = iATS_Service_Request::dpm_url($iats_domain); _iats_civicrm_varset(array('dpmURL' => $dpm_url)); CRM_Core_Resources::singleton()->addScriptFile('com.iatspayments.civicrm', 'js/dpm.js'); } }
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.')); }