Move call to update related pledges on contribution cancel to extension
authoreileen <emcnaughton@wikimedia.org>
Sun, 1 Nov 2020 23:16:31 +0000 (12:16 +1300)
committereileen <emcnaughton@wikimedia.org>
Mon, 2 Nov 2020 02:18:10 +0000 (15:18 +1300)
This moves the functionality over & switches Order api to be entirely relying on the
hook rather than a call to the deprecated transitionComponents function.

CRM/Member/Form/Membership.php
api/v3/Order.php
ext/contributioncancelactions/contributioncancelactions.php
ext/contributioncancelactions/tests/phpunit/CancelTest.php [moved from ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php with 73% similarity]
tests/phpunit/api/v3/OrderTest.php

index 7e57ba159545902d26863ff13be8f645a90b9cb1..0086b47d7ef09688188f63149a376d90dccb3be0 100644 (file)
@@ -1502,6 +1502,7 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
         $params['contribution_id'] = $this->_onlinePendingContributionId;
         $params['componentId'] = $params['id'];
         $params['componentName'] = 'contribute';
+        // Only available statuses are Pending and completed so cancel or failed is not possible here.
         $result = CRM_Contribute_BAO_Contribution::transitionComponents($params, TRUE);
         if (!empty($result) && !empty($params['contribution_id'])) {
           $lineItem = [];
index 0fc784e2f737afc4b14102cbec9b971c8798c5a3..e99b38542c40afd1e00409f01c51e8a74bf80050 100644 (file)
@@ -196,7 +196,6 @@ function civicrm_api3_order_cancel($params) {
   $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
   $params['contribution_status_id'] = array_search('Cancelled', $contributionStatuses);
   $result = civicrm_api3('Contribution', 'create', $params);
-  CRM_Contribute_BAO_Contribution::transitionComponents($params);
   return civicrm_api3_create_success($result['values'], $params, 'Order', 'cancel');
 }
 
index 3cfa1b56a069f5166528b8ca02305c25dec0bd14..61966d72a0bce3497976acfdbddf7b6616e37f98 100644 (file)
@@ -22,10 +22,34 @@ function contributioncancelactions_civicrm_post($op, $objectName, $objectId, $ob
     if ('Cancelled' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $objectRef->contribution_status_id)) {
       contributioncancelactions_cancel_related_pending_memberships((int) $objectId);
       contributioncancelactions_cancel_related_pending_participant_records((int) $objectId);
+      contributioncancelactions_update_related_pledge((int) $objectId, (int) $objectRef->contribution_status_id);
     }
   }
 }
 
+/**
+ * Update any related pledge when a contribution is cancelled.
+ *
+ * This updates the status of the pledge and amount paid.
+ *
+ * The functionality should probably be give more thought in that it currently
+ * does not un-assign the contribution id from the pledge payment. However,
+ * at time of writing the goal is to move rather than fix functionality.
+ *
+ * @param int $contributionID
+ * @param int $contributionStatusID
+ *
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_update_related_pledge(int $contributionID, int $contributionStatusID) {
+  $pledgePayments = civicrm_api3('PledgePayment', 'get', ['contribution_id' => $contributionID])['values'];
+  if (!empty($pledgePayments)) {
+    $pledgePaymentIDS = array_keys($pledgePayments);
+    $pledgePayment = reset($pledgePayments);
+    CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($pledgePayment['pledge_id'], $pledgePaymentIDS, $contributionStatusID);
+  }
+}
+
 /**
  * Find and cancel any pending participant records.
  *
similarity index 73%
rename from ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php
rename to ext/contributioncancelactions/tests/phpunit/CancelTest.php
index 319d6a057482c3813bf69d93874afb99ff80819b..3e850a5428af9ce83fa7118b05265a6087d890c8 100644 (file)
@@ -25,7 +25,7 @@ use Civi\Api4\Participant;
  *
  * @group headless
  */
-class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+class CancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
 
   use \Civi\Test\Api3TestTrait;
 
@@ -65,7 +65,7 @@ class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInter
    * @throws \Civi\API\Exception\UnauthorizedException
    */
   public function testPaypalProCancel() {
-    $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
+    $this->createContact();
     $this->createMembershipType();
     Relationship::create()->setValues([
       'contact_id_a' => $this->ids['contact'][0],
@@ -218,8 +218,71 @@ class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInter
    * @throws \CRM_Core_Exception
    */
   public function testPaypalStandardCancel() {
-    $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
-    $event = Event::create()->setValues(['title' => 'Event', 'start_date' => 'tomorrow', 'event_type_id:name' => 'Workshop'])->execute()->first();
+    $this->createContact();
+    $orderID = $this->createEventOrder();
+    $ipn = new CRM_Core_Payment_PayPalIPN([
+      'mc_gross' => 200,
+      'contactID' => $this->ids['contact'][0],
+      'contributionID' => $orderID,
+      'module' => 'event',
+      'invoice' => 123,
+      'eventID' => $this->ids['event'][0],
+      'participantID' => Participant::get()->addWhere('event_id', '=', $this->ids['event'][0])->addSelect('id')->execute()->first()['id'],
+      'payment_status' => 'Refunded',
+      'processor_id' => $this->createPaymentProcessor(['payment_processor_type_id' => 'PayPal_Standard']),
+    ]);
+    $ipn->main();
+    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+    $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
+  }
+
+  /**
+   * Test cancel order api
+   * @throws API_Exception
+   * @throws CRM_Core_Exception
+   */
+  public function testCancelOrderWithParticipant() {
+    $this->createContact();
+    $orderID = $this->createEventOrder();
+    $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);
+    $this->callAPISuccess('Order', 'get', ['contribution_id' => $orderID]);
+    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+    $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
+  }
+
+  /**
+   * Test cancel order api when a pledge is linked.
+   *
+   * The pledge status should be updated. I believe the contribution should also be unlinked but
+   * the goal at this point is no change.
+   *
+   * @throws CRM_Core_Exception
+   * @throws API_Exception
+   */
+  public function testCancelOrderWithPledge() {
+    $this->createContact();
+    $pledgeID = (int) $this->callAPISuccess('Pledge', 'create', ['contact_id' => $this->ids['contact'][0], 'amount' => 4, 'installments' => 2, 'frequency_unit' => 'month', 'original_installment_amount' => 2, 'create_date' => 'now', 'financial_type_id' => 'Donation', 'start_date' => '+5 days'])['id'];
+    $orderID = (int) $this->callAPISuccess('Order', 'create', ['contact_id' => $this->ids['contact'][0], 'total_amount' => 2, 'financial_type_id' => 'Donation', 'api.Payment.create' => ['total_amount' => 2]])['id'];
+    $pledgePayments = $this->callAPISuccess('PledgePayment', 'get')['values'];
+    $this->callAPISuccess('PledgePayment', 'create', ['id' => key($pledgePayments), 'pledge_id' => $pledgeID, 'contribution_id' => $orderID, 'status_id' => 'Completed', 'actual_amount' => 2]);
+    $beforePledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
+    $this->assertEquals(2, $beforePledge['pledge_total_paid']);
+    $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);
+
+    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+    $afterPledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
+    $this->assertEquals('', $afterPledge['pledge_total_paid']);
+  }
+
+  /**
+   * Create an event and an order for a participant in that event.
+   *
+   * @return int
+   * @throws API_Exception
+   * @throws CRM_Core_Exception
+   */
+  protected function createEventOrder() {
+    $this->ids['event'][0] = (int) Event::create()->setValues(['title' => 'Event', 'start_date' => 'tomorrow', 'event_type_id:name' => 'Workshop'])->execute()->first()['id'];
     $order = $this->callAPISuccess('Order', 'create', [
       'contact_id' => $this->ids['contact'][0],
       'financial_type_id' => 'Donation',
@@ -237,25 +300,21 @@ class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInter
           ],
           'params' => [
             'contact_id' => $this->ids['contact'][0],
-            'event_id' => $event['id'],
+            'event_id' => $this->ids['event'][0],
           ],
         ],
       ],
     ]);
-    $ipn = new CRM_Core_Payment_PayPalIPN([
-      'mc_gross' => 200,
-      'contactID' => $this->ids['contact'][0],
-      'contributionID' => $order['id'],
-      'module' => 'event',
-      'invoice' => 123,
-      'eventID' => $event['id'],
-      'participantID' => Participant::get()->addWhere('event_id', '=', (int) $event['id'])->addSelect('id')->execute()->first()['id'],
-      'payment_status' => 'Refunded',
-      'processor_id' => $this->createPaymentProcessor(['payment_processor_type_id' => 'PayPal_Standard']),
-    ]);
-    $ipn->main();
-    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
-    $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
+    return (int) $order['id'];
+  }
+
+  /**
+   * Create a contact for use in the test.
+   *
+   * @throws API_Exception
+   */
+  protected function createContact(): void {
+    $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
   }
 
 }
index 337f2c54e31432cd52abfac430ce0cb8e96618fc..bf81f4fb6888e01a58e45077e969ebaf4197d62b 100644 (file)
@@ -468,59 +468,6 @@ class api_v3_OrderTest extends CiviUnitTestCase {
     ]);
   }
 
-  /**
-   * Test cancel order api
-   */
-  public function testCancelWithParticipant() {
-    $event = $this->eventCreate();
-    $this->_eventId = $event['id'];
-    $eventParams = [
-      'id' => $this->_eventId,
-      'financial_type_id' => 4,
-      'is_monetary' => 1,
-    ];
-    $this->callAPISuccess('event', 'create', $eventParams);
-    $participantParams = [
-      'financial_type_id' => 4,
-      'event_id' => $this->_eventId,
-      'role_id' => 1,
-      'status_id' => 1,
-      'fee_currency' => 'USD',
-      'contact_id' => $this->_individualId,
-    ];
-    $participant = $this->callAPISuccess('Participant', 'create', $participantParams);
-    $extraParams = [
-      'contribution_mode' => 'participant',
-      'participant_id' => $participant['id'],
-    ];
-    $contribution = $this->addOrder(TRUE, 100, $extraParams);
-    $paymentParticipant = [
-      'participant_id' => $participant['id'],
-      'contribution_id' => $contribution['id'],
-    ];
-    $this->callAPISuccess('ParticipantPayment', 'create', $paymentParticipant);
-    $params = [
-      'contribution_id' => $contribution['id'],
-    ];
-    $this->callAPISuccess('order', 'cancel', $params);
-    $order = $this->callAPISuccess('Order', 'get', $params);
-    $expectedResult = [
-      $contribution['id'] => [
-        'total_amount' => 100,
-        'contribution_id' => $contribution['id'],
-        'contribution_status' => 'Cancelled',
-        'net_amount' => 100,
-      ],
-    ];
-    $this->checkPaymentResult($order, $expectedResult);
-    $participantPayment = $this->callAPISuccess('ParticipantPayment', 'getsingle', $params);
-    $participant = $this->callAPISuccess('participant', 'get', ['id' => $participantPayment['participant_id']]);
-    $this->assertEquals($participant['values'][$participant['id']]['participant_status'], 'Cancelled');
-    $this->callAPISuccess('Contribution', 'Delete', [
-      'id' => $contribution['id'],
-    ]);
-  }
-
   /**
    * Test an exception is thrown if line items do not add up to total_amount, no tax.
    */