Move handling of participant.cancel to contributioncancelactions extension
authoreileen <emcnaughton@wikimedia.org>
Fri, 30 Oct 2020 05:54:01 +0000 (18:54 +1300)
committereileen <emcnaughton@wikimedia.org>
Fri, 30 Oct 2020 21:26:46 +0000 (10:26 +1300)
CRM/Core/Payment/BaseIPN.php
CRM/Core/Payment/PayPalIPN.php
CRM/Core/Payment/PayPalProIPN.php
ext/contributioncancelactions/contributioncancelactions.php
ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php
tests/phpunit/CRM/Core/Payment/BaseIPNTest.php

index daa0988b56470a7aaa02ca6e07c85a975167dafa..7bf95e4710fbc7e4e0e5af20f0905333747e8c9e 100644 (file)
@@ -237,6 +237,7 @@ class CRM_Core_Payment_BaseIPN {
    * @throws \CiviCRM_API3_Exception|\CRM_Core_Exception
    */
   public function cancelled($objects) {
+    CRM_Core_Error::deprecatedFunctionWarning('Use Contribution create api to cancel the contribution');
     $contribution = &$objects['contribution'];
 
     if (empty($contribution->id)) {
@@ -270,6 +271,11 @@ class CRM_Core_Payment_BaseIPN {
           }
         }
       }
+      $participant = &$objects['participant'];
+
+      if ($participant) {
+        $this->cancelParticipant($participant->id);
+      }
     }
     else {
       Contribution::update(FALSE)->setValues([
@@ -277,11 +283,6 @@ class CRM_Core_Payment_BaseIPN {
         'contribution_status_id:name' => 'Cancelled',
       ])->addWhere('id', '=', $contribution->id)->execute();
     }
-    $participant = &$objects['participant'];
-
-    if ($participant) {
-      $this->cancelParticipant($participant->id);
-    }
 
     Civi::log()->debug("Setting contribution status to Cancelled");
     return TRUE;
@@ -309,6 +310,8 @@ class CRM_Core_Payment_BaseIPN {
    * Logic to cancel a participant record when the related contribution changes to failed/cancelled.
    * @todo This is part of a bigger refactor for dev/core/issues/927 - "duplicate" functionality exists in CRM_Contribute_BAO_Contribution::cancel()
    *
+   * @deprecated
+   *
    * @param $participantID
    *
    * @throws \CiviCRM_API3_Exception
index 9e1a6050e36e2a5941abd6002ebce085f94d43bf..77a7ee684407dcc9c0c8412cd36332aaa8025eda 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  *
  * @package CRM
@@ -348,7 +350,11 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
         return;
       }
       if ($status === 'Refunded' || $status === 'Reversed') {
-        $this->cancelled($objects);
+        Contribution::update(FALSE)->setValues([
+          'cancel_date' => 'now',
+          'contribution_status_id:name' => 'Cancelled',
+        ])->addWhere('id', '=', $contributionID)->execute();
+        Civi::log()->debug("Setting contribution status to Cancelled");
         return;
       }
       if ($status !== 'Completed') {
index 5f48eb3d481bbe7b46977f68e92250d1ae7f3453..ff8cf081785cdc81a0e1e81cba5397e2b5dec0ea 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  *
  * @package CRM
@@ -327,8 +329,12 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN {
       Civi::log()->debug('Returning since contribution status is Pending');
       return;
     }
-    elseif ($status === 'Refunded' || $status === 'Reversed') {
-      $this->cancelled($objects);
+    if ($status === 'Refunded' || $status === 'Reversed') {
+      Contribution::update(FALSE)->setValues([
+        'cancel_date' => 'now',
+        'contribution_status_id:name' => 'Cancelled',
+      ])->addWhere('id', '=', $contribution->id)->execute();
+      Civi::log()->debug("Setting contribution status to Cancelled");
       return;
     }
     elseif ($status !== 'Completed') {
index 2ec3aae284fa85aef5c403b27c3158e63d7d898d..3cfa1b56a069f5166528b8ca02305c25dec0bd14 100644 (file)
@@ -5,6 +5,7 @@ require_once 'contributioncancelactions.civix.php';
 use CRM_Contributioncancelactions_ExtensionUtil as E;
 // phpcs:enable
 use Civi\Api4\LineItem;
+use Civi\Api4\Participant;
 
 /**
  * Implements hook_civicrm_preProcess().
@@ -19,25 +20,59 @@ use Civi\Api4\LineItem;
 function contributioncancelactions_civicrm_post($op, $objectName, $objectId, $objectRef) {
   if ($op === 'edit' && $objectName === 'Contribution') {
     if ('Cancelled' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $objectRef->contribution_status_id)) {
-      // Find and cancel any pending memberships.
-      $connectedMemberships = (array) LineItem::get(FALSE)->setWhere([
-        ['contribution_id', '=', $objectId],
-        ['entity_table', '=', 'civicrm_membership'],
-      ])->execute()->indexBy('entity_id');
-      if (empty($connectedMemberships)) {
-        return;
-      }
-      // @todo we don't have v4 membership api yet so v3 for now.
-      $connectedMemberships = array_keys(civicrm_api3('Membership', 'get', [
-        'status_id' => 'Pending',
-        'id' => ['IN' => array_keys($connectedMemberships)],
-      ])['values']);
-      if (empty($connectedMemberships)) {
-        return;
-      }
-      foreach ($connectedMemberships as $membershipID) {
-        civicrm_api3('Membership', 'create', ['status_id' => 'Cancelled', 'id' => $membershipID, 'is_override' => 1]);
-      }
+      contributioncancelactions_cancel_related_pending_memberships((int) $objectId);
+      contributioncancelactions_cancel_related_pending_participant_records((int) $objectId);
     }
   }
 }
+
+/**
+ * Find and cancel any pending participant records.
+ *
+ * @param int $contributionID
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_cancel_related_pending_participant_records($contributionID): void {
+  $pendingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Pending'");
+  $waitingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Waiting'");
+  $cancellableParticipantRecords = civicrm_api3('ParticipantPayment', 'get', [
+    'contribution_id' => $contributionID,
+    'participant_id.status_id' => ['IN' => array_merge(array_keys($pendingStatuses), array_keys($waitingStatuses))],
+  ])['values'];
+  if (empty($cancellableParticipantRecords)) {
+    return;
+  }
+  Participant::update(FALSE)
+    ->addWhere('id', 'IN', array_keys($cancellableParticipantRecords))
+    ->setValues(['status_id:name' => 'Cancelled'])
+    ->execute();
+}
+
+/**
+ * Find and cancel any pending memberships.
+ *
+ * @param int $contributionID
+ * @throws API_Exception
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_cancel_related_pending_memberships($contributionID): void {
+  $connectedMemberships = (array) LineItem::get(FALSE)->setWhere([
+    ['contribution_id', '=', $contributionID],
+    ['entity_table', '=', 'civicrm_membership'],
+  ])->execute()->indexBy('entity_id');
+
+  if (empty($connectedMemberships)) {
+    return;
+  }
+  // @todo we don't have v4 membership api yet so v3 for now.
+  $connectedMemberships = array_keys(civicrm_api3('Membership', 'get', [
+    'status_id' => 'Pending',
+    'id' => ['IN' => array_keys($connectedMemberships)],
+  ])['values']);
+  if (empty($connectedMemberships)) {
+    return;
+  }
+  foreach ($connectedMemberships as $membershipID) {
+    civicrm_api3('Membership', 'create', ['status_id' => 'Cancelled', 'id' => $membershipID, 'is_override' => 1]);
+  }
+}
index 95816b6df54b3d80bb12052dac22012667f53bee..319d6a057482c3813bf69d93874afb99ff80819b 100644 (file)
@@ -7,6 +7,9 @@ use Civi\Api4\Contact;
 use Civi\Api4\MembershipType;
 use Civi\Api4\RelationshipType;
 use Civi\Api4\Relationship;
+use Civi\Api4\Event;
+use Civi\Api4\PriceField;
+use Civi\Api4\Participant;
 
 /**
  * FIXME - Add test description.
@@ -208,4 +211,51 @@ class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInter
     return (int) $result['id'];
   }
 
+  /**
+   * Test that a cancel from paypal pro results in an order being cancelled.
+   *
+   * @throws \API_Exception
+   * @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();
+    $order = $this->callAPISuccess('Order', 'create', [
+      'contact_id' => $this->ids['contact'][0],
+      'financial_type_id' => 'Donation',
+      'invoice_id' => 123,
+      'line_items' => [
+        [
+          'line_item' => [
+            [
+              'line_total' => 5,
+              'qty' => 1,
+              'financial_type_id' => 1,
+              'entity_table' => 'civicrm_participant',
+              'price_field_id' => PriceField::get()->addSelect('id')->addWhere('name', '=', 'contribution_amount')->execute()->first()['id'],
+            ],
+          ],
+          'params' => [
+            'contact_id' => $this->ids['contact'][0],
+            'event_id' => $event['id'],
+          ],
+        ],
+      ],
+    ]);
+    $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);
+  }
+
 }
index 52d9426fbfc038038d5e99bf85aba28134b855f3..dac135b09919ccddfcd9128e5ce08f76a40186dc 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  * Class CRM_Core_Payment_BaseIPNTest
  * @group headless
@@ -405,15 +407,16 @@ class CRM_Core_Payment_BaseIPNTest extends CiviUnitTestCase {
 
   public function testThatCancellingEventPaymentWillCancelAllAdditionalPendingParticipantsAndCreateCancellationActivities() {
     $this->_setUpParticipantObjects('Pending from incomplete transaction');
-    $this->IPN->loadObjects($this->input, $this->ids, $this->objects, FALSE, $this->_processorId);
     $additionalParticipantId = $this->participantCreate([
       'event_id' => $this->_eventId,
       'registered_by_id' => $this->_participantId,
       'status_id' => 'Pending from incomplete transaction',
     ]);
 
-    $transaction = new CRM_Core_Transaction();
-    $this->IPN->cancelled($this->objects);
+    Contribution::update(FALSE)->setValues([
+      'cancel_date' => 'now',
+      'contribution_status_id:name' => 'Cancelled',
+    ])->addWhere('id', '=', $this->_contributionId)->execute();
 
     $cancelledParticipantsCount = civicrm_api3('Participant', 'get', [
       'sequential' => 1,