CRM-17655 Adding a payment to a recurring contribution sequence should update the...
authoreileenmcnaughton <eileen@fuzion.co.nz>
Thu, 3 Dec 2015 09:32:03 +0000 (09:32 +0000)
committereileenmcnaugton <eileen@fuzion.co.nz>
Wed, 9 Dec 2015 01:10:25 +0000 (14:10 +1300)
CRM/Contribute/BAO/Contribution.php
CRM/Contribute/BAO/ContributionRecur.php
tests/phpunit/api/v3/ContributionPageTest.php
tests/phpunit/api/v3/ContributionTest.php

index 79bebb63b614987437e9135c7da10d2d8884e434..12c5ee398cff9f096b86fe8c605238d69b0dee81 100644 (file)
@@ -200,6 +200,13 @@ class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution {
     $params['contribution'] = $contribution;
     self::recordFinancialAccounts($params);
 
+    if (self::isUpdateToRecurringContribution($params)) {
+      CRM_Contribute_BAO_ContributionRecur::updateOnNewPayment(
+        $params['contribution_recur_id'],
+        $contributionStatus[$params['contribution_status_id']]
+      );
+    }
+
     // reset the group contact cache for this group
     CRM_Contact_BAO_GroupContactCache::remove();
 
@@ -213,6 +220,33 @@ class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution {
     return $result;
   }
 
+  /**
+   * Is this contribution updating an existing recurring contribution.
+   *
+   * We need to upd the status of the linked recurring contribution if we have a new payment against it, or the initial
+   * pending payment is being confirmed (or failing).
+   *
+   * @param array $params
+   *
+   * @return bool
+   */
+  public static function isUpdateToRecurringContribution($params) {
+    if (!empty($params['contribution_recur_id']) && empty($params['id'])) {
+      return TRUE;
+    }
+    if (empty($params['prevContribution']) || empty($params['contribution_status_id'])) {
+      return FALSE;
+    }
+    if (empty($params['contribution_recur_id']) && empty($params['prevContribution']->contribution_recur_id)) {
+      return FALSE;
+    }
+    $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
+    if ($params['prevContribution']->contribution_status_id == array_search('Pending', $contributionStatus)) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
   /**
    * Get defaults for new entity.
    * @return array
index ac4d40faf4c1a16a73978c19e2a005bdafebace3..ca7c4009a4eee831881cc1c539c7f81dd81834fc 100644 (file)
@@ -770,4 +770,79 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
     );
   }
 
+  /**
+   * Update recurring contribution based on incoming payment.
+   *
+   * Do not rename or move this function without updating https://issues.civicrm.org/jira/browse/CRM-17655.
+   *
+   * @param int $recurringContributionID
+   * @param string $paymentStatus
+   *   Payment status - this correlates to the machine name of the contribution status ID ie
+   *   - Completed
+   *   - Failed
+   *
+   * @throws \CiviCRM_API3_Exception
+   */
+  public static function updateOnNewPayment($recurringContributionID, $paymentStatus) {
+    if (!in_array($paymentStatus, array('Completed', 'Failed'))) {
+      return;
+    }
+    $params = array(
+      'id' => $recurringContributionID,
+      'return' => array(
+        'contribution_status_id',
+        'next_sched_contribution_date',
+        'frequency_unit',
+        'frequency_interval',
+        'installments',
+        'failure_count',
+      ),
+    );
+
+    $existing = civicrm_api3('ContributionRecur', 'getsingle', $params);
+
+    if ($paymentStatus == 'Completed'
+      && CRM_Contribute_PseudoConstant::contributionStatus($existing['contribution_status_id'], 'name') == 'Pending') {
+      $params['contribution_status_id'] = 'In Progress';
+    }
+    if ($paymentStatus == 'Failed') {
+      $params['failure_count'] = $existing['failure_count'];
+    }
+    $params['modified_date'] = date('Y-m-d H:i:s');
+
+    if (!empty($existing['installments']) && self::isComplete($recurringContributionID, $existing['installments'])) {
+      $params['contribution_status_id'] = 'Completed';
+    }
+    else {
+      // Only update next sched date if it's empty or 'just now' because payment processors may be managing
+      // the scheduled date themselves as core did not previously provide any help.
+      if (empty($params['next_sched_contribution_date']) || strtotime($params['next_sched_contribution_date']) ==
+        strtotime(date('Y-m-d'))) {
+        $params['next_sched_contribution_date'] = date('Y-m-d', strtotime('+' . $existing['frequency_interval'] . ' ' . $existing['frequency_unit']));
+      }
+    }
+    civicrm_api3('ContributionRecur', 'create', $params);
+  }
+
+  /**
+   * Is this recurring contribution now complete.
+   *
+   * Have all the payments expected been received now.
+   *
+   * @param int $recurringContributionID
+   * @param int $installments
+   *
+   * @return bool
+   */
+  protected static function isComplete($recurringContributionID, $installments) {
+    $paidInstallments = CRM_Core_DAO::singleValueQuery(
+      'SELECT count(*) FROM civicrm_contribution WHERE id = %1',
+      array(1 => array($recurringContributionID, 'Integer'))
+    );
+    if ($paidInstallments >= $installments) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
 }
index 5aae15a30e884ac64d24e5dbed871a7585167919..228c3f8836cccccf7da8c7247ae16d730efc5a92 100644 (file)
@@ -641,7 +641,7 @@ class api_v3_ContributionPageTest extends CiviUnitTestCase {
     $renewedMembership = $this->callAPISuccessGetSingle('membership', array('id' => $membershipPayment['membership_id']));
     $this->assertEquals(date('Y-m-d', strtotime('+ 1 year', strtotime($membership['end_date']))), $renewedMembership['end_date']);
     $recurringContribution = $this->callAPISuccess('contribution_recur', 'getsingle', array('id' => $contribution['contribution_recur_id']));
-    $this->assertEquals(2, $recurringContribution['contribution_status_id']);
+    $this->assertEquals(5, $recurringContribution['contribution_status_id']);
   }
 
   /**
index 5b5c3bbb503ca6304c9b620fa8e117c4b995f20c..664f4f99d1c06aed5879493d7758e112a5d2b75f 100644 (file)
@@ -1556,6 +1556,40 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
     $mut->stop();
   }
 
+  /**
+   * Test completing first transaction in a recurring series.
+   *
+   * The status should be set to 'in progress' and the next scheduled payment date calculated.
+   */
+  public function testCompleteTransactionSetStatusToInProgress() {
+    $paymentProcessorID = $this->paymentProcessorCreate();
+    $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', array(
+      'contact_id' => $this->_individualId,
+      'installments' => '12',
+      'frequency_interval' => '1',
+      'amount' => '500',
+      'contribution_status_id' => 'Pending',
+      'start_date' => '2012-01-01 00:00:00',
+      'currency' => 'USD',
+      'frequency_unit' => 'month',
+      'payment_processor_id' => $paymentProcessorID,
+    ));
+    $contribution = $this->callAPISuccess('contribution', 'create', array_merge(
+      $this->_params,
+      array(
+        'contribution_recur_id' => $contributionRecur['id'],
+        'contribution_status_id' => 'Pending',
+      ))
+    );
+    $this->callAPISuccess('Contribution', 'completetransaction', array('id' => $contribution));
+    $contributionRecur = $this->callAPISuccessGetSingle('ContributionRecur', array(
+      'id' => $contributionRecur['id'],
+      'return' => array('next_sched_contribution_date', 'contribution_status_id'),
+    ));
+    $this->assertEquals(5, $contributionRecur['contribution_status_id']);
+    $this->assertEquals(date('Y-m-d 00:00:00', strtotime('+1 month')), $contributionRecur['next_sched_contribution_date']);
+  }
+
   /**
    * Test completing a transaction with an event via the API.
    *