From: eileen Date: Sun, 1 Nov 2020 23:16:31 +0000 (+1300) Subject: Move call to update related pledges on contribution cancel to extension X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=4f1cfdfa618f4e1d1f88f0f39fdb106d6d84e9b7;p=civicrm-core.git Move call to update related pledges on contribution cancel to extension This moves the functionality over & switches Order api to be entirely relying on the hook rather than a call to the deprecated transitionComponents function. --- diff --git a/CRM/Member/Form/Membership.php b/CRM/Member/Form/Membership.php index 7e57ba1595..0086b47d7e 100644 --- a/CRM/Member/Form/Membership.php +++ b/CRM/Member/Form/Membership.php @@ -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 = []; diff --git a/api/v3/Order.php b/api/v3/Order.php index 0fc784e2f7..e99b38542c 100644 --- a/api/v3/Order.php +++ b/api/v3/Order.php @@ -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'); } diff --git a/ext/contributioncancelactions/contributioncancelactions.php b/ext/contributioncancelactions/contributioncancelactions.php index 3cfa1b56a0..61966d72a0 100644 --- a/ext/contributioncancelactions/contributioncancelactions.php +++ b/ext/contributioncancelactions/contributioncancelactions.php @@ -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. * diff --git a/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php b/ext/contributioncancelactions/tests/phpunit/CancelTest.php similarity index 73% rename from ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php rename to ext/contributioncancelactions/tests/phpunit/CancelTest.php index 319d6a0574..3e850a5428 100644 --- a/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php +++ b/ext/contributioncancelactions/tests/phpunit/CancelTest.php @@ -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']; } } diff --git a/tests/phpunit/api/v3/OrderTest.php b/tests/phpunit/api/v3/OrderTest.php index 337f2c54e3..bf81f4fb68 100644 --- a/tests/phpunit/api/v3/OrderTest.php +++ b/tests/phpunit/api/v3/OrderTest.php @@ -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. */