From a201e2089960c388554d1b98f2f388b73f6468c3 Mon Sep 17 00:00:00 2001 From: eileen Date: Fri, 30 Oct 2020 18:54:01 +1300 Subject: [PATCH] Move handling of participant.cancel to contributioncancelactions extension --- CRM/Core/Payment/BaseIPN.php | 13 ++-- CRM/Core/Payment/PayPalIPN.php | 8 +- CRM/Core/Payment/PayPalProIPN.php | 10 ++- .../contributioncancelactions.php | 73 ++++++++++++++----- .../tests/phpunit/IPNCancelTest.php | 50 +++++++++++++ .../phpunit/CRM/Core/Payment/BaseIPNTest.php | 9 ++- 6 files changed, 133 insertions(+), 30 deletions(-) diff --git a/CRM/Core/Payment/BaseIPN.php b/CRM/Core/Payment/BaseIPN.php index daa0988b56..7bf95e4710 100644 --- a/CRM/Core/Payment/BaseIPN.php +++ b/CRM/Core/Payment/BaseIPN.php @@ -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 diff --git a/CRM/Core/Payment/PayPalIPN.php b/CRM/Core/Payment/PayPalIPN.php index 9e1a6050e3..77a7ee6844 100644 --- a/CRM/Core/Payment/PayPalIPN.php +++ b/CRM/Core/Payment/PayPalIPN.php @@ -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') { diff --git a/CRM/Core/Payment/PayPalProIPN.php b/CRM/Core/Payment/PayPalProIPN.php index 5f48eb3d48..ff8cf08178 100644 --- a/CRM/Core/Payment/PayPalProIPN.php +++ b/CRM/Core/Payment/PayPalProIPN.php @@ -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') { diff --git a/ext/contributioncancelactions/contributioncancelactions.php b/ext/contributioncancelactions/contributioncancelactions.php index 2ec3aae284..3cfa1b56a0 100644 --- a/ext/contributioncancelactions/contributioncancelactions.php +++ b/ext/contributioncancelactions/contributioncancelactions.php @@ -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]); + } +} diff --git a/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php b/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php index 95816b6df5..319d6a0574 100644 --- a/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php +++ b/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php @@ -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); + } + } diff --git a/tests/phpunit/CRM/Core/Payment/BaseIPNTest.php b/tests/phpunit/CRM/Core/Payment/BaseIPNTest.php index 52d9426fbf..dac135b099 100644 --- a/tests/phpunit/CRM/Core/Payment/BaseIPNTest.php +++ b/tests/phpunit/CRM/Core/Payment/BaseIPNTest.php @@ -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, -- 2.25.1