Skip to content

Commit

Permalink
Better handling of recurring monthlies initiating after the 28th
Browse files Browse the repository at this point in the history
  • Loading branch information
adixon committed Oct 27, 2020
1 parent f6056f1 commit b589f88
Show file tree
Hide file tree
Showing 2 changed files with 325 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CRM/Core/Payment/iATSService.php
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,13 @@ protected function updateRecurring($params, $update) {
// By default, it's empty, unless we've got a future start date.
if (empty($update['receive_date'])) {
$next = strtotime('+' . $params['frequency_interval'] . ' ' . $params['frequency_unit']);
// handle the special case of monthly contributions made after the 28th
if ('month' == $params['frequency_unit']) {
$now = getdate();
if ($now['mday'] > 28) { // pull it back to the 28th
$next = $next - (($now['mday'] - 28) * 24 * 60 * 60);
}
}
$recur_update['next_sched_contribution_date'] = date('Ymd', $next) . '030000';
}
else {
Expand Down
318 changes: 318 additions & 0 deletions iats.php
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,321 @@ function iats_civicrm_buildForm_CRM_Contribute_Form_UpdateBilling(&$form) {
$form->addElement('hidden', 'crid', $crid);
}
}

/**
* Implementation of hook_civicrm_alterSettingsFolders.
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
*/
function iats_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
_iats_civix_civicrm_alterSettingsFolders($metaDataFolders);
}

/**
* For a recurring contribution, find a reasonable candidate for a template, where possible.
*/
function _iats_civicrm_getContributionTemplate($contribution) {
// Get the first contribution in this series that matches the same total_amount, if present.
$template = array();
$get = array('contribution_recur_id' => $contribution['contribution_recur_id'], 'options' => array('sort' => ' id', 'limit' => 1));
if (!empty($contribution['total_amount'])) {
$get['total_amount'] = $contribution['total_amount'];
}
$result = civicrm_api3('contribution', 'get', $get);
if (!empty($result['values'])) {
$contribution_ids = array_keys($result['values']);
$template = $result['values'][$contribution_ids[0]];
$template['original_contribution_id'] = $contribution_ids[0];
$template['line_items'] = array();
$get = array('entity_table' => 'civicrm_contribution', 'entity_id' => $contribution_ids[0]);
$result = civicrm_api3('LineItem', 'get', $get);
if (!empty($result['values'])) {
foreach ($result['values'] as $initial_line_item) {
$line_item = array();
foreach (array('price_field_id', 'qty', 'line_total', 'unit_price', 'label', 'price_field_value_id', 'financial_type_id') as $key) {
$line_item[$key] = $initial_line_item[$key];
}
$template['line_items'][] = $line_item;
}
}
}
return $template;
}

/**
* Function _iats_contributionrecur_next.
*
* @param $from_time: a unix time stamp, the function returns values greater than this
* @param $days: an array of allowable days of the month
*
* A utility function to calculate the next available allowable day, starting from $from_time.
* Strategy: increment the from_time by one day until the day of the month matches one of my available days of the month.
*/
function _iats_contributionrecur_next($from_time, $allow_mdays) {
$dp = getdate($from_time);
// So I don't get into an infinite loop somehow.
$i = 0;
while (($i++ < 60) && !in_array($dp['mday'], $allow_mdays)) {
$from_time += (24 * 60 * 60);
$dp = getdate($from_time);
}
return $from_time;
}

/**
* Function _iats_contribution_payment
*
* @param $contribution an array of a contribution to be created (or in case of future start date,
possibly an existing pending contribution to recycle, if it already has a contribution id).
* @param $options must include customer code, subtype and iats_domain, may include a membership id
* @param $original_contribution_id if included, use as a template for a recurring contribution.
*
* A high-level utility function for making a contribution payment from an existing recurring schedule
* Used in the Iatsrecurringcontributions.php job and the one-time ('card on file') form.
*
* Since 4.7.12, we can are using the new repeattransaction api.
*/
function _iats_process_contribution_payment(&$contribution, $options, $original_contribution_id) {
// By default, don't use repeattransaction
$use_repeattransaction = FALSE;
$is_recurrence = !empty($original_contribution_id);
// First try and get the money with iATS Payments, using my cover function.
// TODO: convert this into an api job?
$result = _iats_process_transaction($contribution, $options);

// Handle any case of a failure of some kind, either the card failed, or the system failed.
if (empty($result['status'])) {
/* set the failed transaction status, or pending if I had a server issue */
$contribution['contribution_status_id'] = empty($result['auth_result']) ? 2 : 4;
/* and include the reason in the source field */
$contribution['source'] .= ' ' . $result['reasonMessage'];
// Save my reject code here for processing by the calling function (a bit lame)
$contribution['iats_reject_code'] = $result['auth_result'];
}
else {
// I have a transaction id.
$trxn_id = $contribution['trxn_id'] = trim($result['remote_id']) . ':' . time();
// Initialize the status to pending
$contribution['contribution_status_id'] = 2;
// We'll use the repeattransaction api for successful transactions under three conditions:
// 1. if we want it
// 2. if we don't already have a contribution id
// 3. if we trust it
$use_repeattransaction = $is_recurrence && empty($contribution['id']) && _iats_civicrm_use_repeattransaction();
}
if ($use_repeattransaction) {
// We processed it successflly and I can try to use repeattransaction.
// Requires the original contribution id.
// Issues with this api call:
// 1. Always triggers an email and doesn't include trxn.
// 2. Date is wrong.
try {
// $status = $result['contribution_status_id'] == 1 ? 'Completed' : 'Pending';
$contributionResult = civicrm_api3('Contribution', 'repeattransaction', array(
'original_contribution_id' => $original_contribution_id,
'contribution_status_id' => 'Pending',
'is_email_receipt' => 0,
// 'invoice_id' => $contribution['invoice_id'],
///'receive_date' => $contribution['receive_date'],
// 'campaign_id' => $contribution['campaign_id'],
// 'financial_type_id' => $contribution['financial_type_id'],.
// 'payment_processor_id' => $contribution['payment_processor'],
'contribution_recur_id' => $contribution['contribution_recur_id'],
));

// watchdog('iats_civicrm','repeat transaction result <pre>@params</pre>',array('@params' => print_r($pending,TRUE)));.
$contribution['id'] = CRM_Utils_Array::value('id', $contributionResult);
}
catch (Exception $e) {
// Ignore this, though perhaps I should log it.
}
if (empty($contribution['id'])) {
// Assume I failed completely and I'll fall back to doing it the manual way.
$use_repeattransaction = FALSE;
}
else {
// If repeattransaction succeded.
// First restore/add various fields that the repeattransaction api may overwrite or ignore.
// TODO - fix this in core to allow these to be set above.
civicrm_api3('contribution', 'create', array('id' => $contribution['id'],
'invoice_id' => $contribution['invoice_id'],
'source' => $contribution['source'],
'receive_date' => $contribution['receive_date'],
'payment_instrument_id' => $contribution['payment_instrument_id'],
// '' => $contribution['receive_date'],
));
// Save my status in the contribution array that was passed in.
$contribution['contribution_status_id'] = $result['contribution_status_id'];
if ($result['contribution_status_id'] == 1) {
// My transaction completed, so record that fact in CiviCRM, potentially sending an invoice.
try {
civicrm_api3('Contribution', 'completetransaction', array(
'id' => $contribution['id'],
'payment_processor_id' => $contribution['payment_processor'],
'is_email_receipt' => (empty($options['is_email_receipt']) ? 0 : 1),
'trxn_id' => $contribution['trxn_id'],
'receive_date' => $contribution['receive_date'],
));
}
catch (Exception $e) {
// log the error and continue
CRM_Core_Error::debug_var('Unexpected Exception', $e);
}
}
else {
// just save my trxn_id for ACH/EFT verification later
try {
civicrm_api3('Contribution', 'create', array(
'id' => $contribution['id'],
'trxn_id' => $contribution['trxn_id'],
));
}
catch (Exception $e) {
// log the error and continue
CRM_Core_Error::debug_var('Unexpected Exception', $e);
}
}
}
}
if (!$use_repeattransaction) {
/* If I'm not using repeattransaction for any reason, I'll create the contribution manually */
// This code assumes that the contribution_status_id has been set properly above, either pending or failed.
$contributionResult = civicrm_api3('contribution', 'create', $contribution);
// Pass back the created id indirectly since I'm calling by reference.
$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.
}
}
/* And then I'm done unless it completed */
if ($result['contribution_status_id'] == 1 && !empty($result['status'])) {
/* success, and the transaction has completed */
$complete = array('id' => $contribution['id'],
'payment_processor_id' => $contribution['payment_processor'],
'trxn_id' => $trxn_id,
'receive_date' => $contribution['receive_date']
);
$complete['is_email_receipt'] = empty($options['is_email_receipt']) ? 0 : 1;
try {
$contributionResult = civicrm_api3('contribution', 'completetransaction', $complete);
}
catch (Exception $e) {
// Don't throw an exception here, or else I won't have updated my next contribution date for example.
$contribution['source'] .= ' [with unexpected api.completetransaction error: ' . $e->getMessage() . ']';
}
// Restore my source field that ipn code 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'));
$message = $is_recurrence ? ts('Successfully processed contribution in recurring series id %1: ', array(1 => $contribution['contribution_recur_id'])) : ts('Successfully processed one-time contribution: ');
return $message . $result['auth_result'];
}
}
// Now return the appropriate message.
if (empty($result['status'])) {
return ts('Failed to process recurring contribution id %1: ', array(1 => $contribution['contribution_recur_id'])) . $result['reasonMessage'];
}
elseif ($result['contribution_status_id'] == 1) {
return ts('Successfully processed recurring contribution in series id %1: ', array(1 => $contribution['contribution_recur_id'])) . $result['auth_result'];
}
else {
// I'm using ACH/EFT or a processor that doesn't complete.
return ts('Successfully processed pending recurring contribution in series id %1: ', array(1 => $contribution['contribution_recur_id'])) . $result['auth_result'];
}
}

/**
* Function _iats_process_transaction.
*
* @param $contribution an array of properties of a contribution to be processed
* @param $options must include customer code, subtype and iats_domain
*
* A low-level utility function for triggering a transaction on iATS.
*/
function _iats_process_transaction($contribution, $options) {
require_once "CRM/iATS/iATSService.php";
switch ($options['subtype']) {
case 'ACHEFT':
$method = 'acheft_with_customer_code';
// Will not complete.
$contribution_status_id = 2;
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']);
// remove the customerIPAddress if it's the internal loopback to prevent
// being locked out due to velocity checks
if ('127.0.0.1' == $request['customerIPAddress']) {
$request['customerIPAddress'] = '';
}

// Make the soap request.
$response = $iats->request($credentials, $request);
// Process the soap response into a readable result.
$result = $iats->result($response);
// pass back the anticipated status_id based on the method (i.e. 1 for CC, 2 for ACH/EFT)
$result['contribution_status_id'] = $contribution_status_id;
return $result;
}

/**
* Function _iats_get_future_start_dates
*
* @param $start_date a timestamp, only return dates after this.
* @param $allow_days an array of allowable days of the month.
*
* A low-level utility function for triggering a transaction on iATS.
*/
function _iats_get_future_monthly_start_dates($start_date, $allow_days) {
// Future date options.
$start_dates = array();
// special handling for today - it means immediately or now.
$today = date('Ymd').'030000';
$allow_any_day = (max($allow_days) <= 0) ? TRUE : FALSE;
// If not set, only allow for the first 28 days of the month.
if ($allow_any_day) {
$allow_days = range(1,28);
}
for ($j = 0; $j < count($allow_days); $j++) {
// So I don't get into an infinite loop somehow ..
$i = 0;
$dp = getdate($start_date);
while (($i++ < 60) && !in_array($dp['mday'], $allow_days)) {
$start_date += (24 * 60 * 60);
$dp = getdate($start_date);
}
$key = date('Ymd', $start_date).'030000';
if ($key == $today) { // special handling
$display = ts('Now');
$key = ''; // date('YmdHis');
}
else {
$display = strftime('%B %e, %Y', $start_date);
}
$start_dates[$key] = $display;
$start_date += (24 * 60 * 60);
}
// A weird special case: if today is > 28 and I haven't explicitly set allowable days
if ($allow_any_day && !isset($start_dates[''])) {
$start_dates[''] = ts('Now');
ksort($start_dates);
}
return $start_dates;
}

0 comments on commit b589f88

Please sign in to comment.