From f78c925844292600399db8a488fe9ed3837c554d Mon Sep 17 00:00:00 2001 From: eileen Date: Wed, 6 Jul 2016 21:07:08 +1200 Subject: [PATCH] Refactor for std to support testing & replaying --- CRM/Core/Payment/PayPalIPN.php | 77 ++++---- extern/ipn.php | 2 +- .../phpunit/CRM/Core/Payment/PayPalPNTest.php | 167 ++++++++++++++++++ 3 files changed, 213 insertions(+), 33 deletions(-) create mode 100644 tests/phpunit/CRM/Core/Payment/PayPalPNTest.php diff --git a/CRM/Core/Payment/PayPalIPN.php b/CRM/Core/Payment/PayPalIPN.php index c540473676..e9e0cf093b 100644 --- a/CRM/Core/Payment/PayPalIPN.php +++ b/CRM/Core/Payment/PayPalIPN.php @@ -37,28 +37,42 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { static $_paymentProcessor = NULL; /** - * Constructor. + * Input parameters from payment processor. Store these so that + * the code does not need to keep retrieving from the http request + * @var array */ - public function __construct() { + protected $_inputParameters = array(); + + /** + * Constructor function. + * + * @param array $inputData + * Contents of HTTP REQUEST. + * + * @throws CRM_Core_Exception + */ + public function __construct($inputData) { + $this->setInputParameters($inputData); parent::__construct(); } /** * @param string $name * @param $type - * @param string $location * @param bool $abort * * @return mixed */ - public static function retrieve($name, $type, $location = 'POST', $abort = TRUE) { + public function retrieve($name, $type, $abort = TRUE) { static $store = NULL; - $value = CRM_Utils_Request::retrieve($name, $type, $store, - FALSE, NULL, $location + $value = CRM_Utils_Type::validate( + CRM_Utils_Array::value($name, $this->_inputParameters), + $type, + FALSE ); if ($abort && $value === NULL) { CRM_Core_Error::debug_log_message("Could not find an entry for $name in $location"); - echo "Failure: Missing Parameter

"; + echo "Failure: Missing Parameter

" . $name; exit(); } return $value; @@ -110,7 +124,7 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { $sendNotification = FALSE; $subscriptionPaymentStatus = NULL; //set transaction type - $txnType = $_POST['txn_type']; + $txnType = $this->retrieve('txn_type', 'String'); switch ($txnType) { case 'subscr_signup': $recur->create_date = $now; @@ -122,7 +136,7 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { if ($statusID != 5) { $recur->contribution_status_id = 2; } - $recur->processor_id = $_POST['subscr_id']; + $recur->processor_id = $this->retrieve('subscr_id', 'String'); $recur->trxn_id = $recur->processor_id; $sendNotification = TRUE; $subscriptionPaymentStatus = CRM_Core_Payment::RECURRING_PAYMENT_START; @@ -296,36 +310,35 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { * @return bool */ public function main() { - //@todo - this could be refactored like PayPalProIPN & a test could be added $objects = $ids = $input = array(); - $component = CRM_Utils_Array::value('module', $_GET); + $component = $this->retrieve('module', 'String'); $input['component'] = $component; - // get the contribution and contact ids from the GET params - $ids['contact'] = self::retrieve('contactID', 'Integer', 'GET', TRUE); - $ids['contribution'] = self::retrieve('contributionID', 'Integer', 'GET', TRUE); + $ids['contact'] = $this->retrieve('contactID', 'Integer', TRUE); + $ids['contribution'] = $this->retrieve('contributionID', 'Integer', TRUE); $this->getInput($input, $ids); if ($component == 'event') { - $ids['event'] = self::retrieve('eventID', 'Integer', 'GET', TRUE); - $ids['participant'] = self::retrieve('participantID', 'Integer', 'GET', TRUE); + $ids['event'] = $this->retrieve('eventID', 'Integer', TRUE); + $ids['participant'] = $this->retrieve('participantID', 'Integer', TRUE); } else { // get the optional ids - $ids['membership'] = self::retrieve('membershipID', 'Integer', 'GET', FALSE); - $ids['contributionRecur'] = self::retrieve('contributionRecurID', 'Integer', 'GET', FALSE); - $ids['contributionPage'] = self::retrieve('contributionPageID', 'Integer', 'GET', FALSE); - $ids['related_contact'] = self::retrieve('relatedContactID', 'Integer', 'GET', FALSE); - $ids['onbehalf_dupe_alert'] = self::retrieve('onBehalfDupeAlert', 'Integer', 'GET', FALSE); + $ids['membership'] = $this->retrieve('membershipID', 'Integer', FALSE); + $ids['contributionRecur'] = $this->retrieve('contributionRecurID', 'Integer', FALSE); + $ids['contributionPage'] = $this->retrieve('contributionPageID', 'Integer', FALSE); + $ids['related_contact'] = $this->retrieve('relatedContactID', 'Integer', FALSE); + $ids['onbehalf_dupe_alert'] = $this->retrieve('onBehalfDupeAlert', 'Integer', FALSE); } $processorParams = array( - 'user_name' => self::retrieve('receiver_email', 'String', 'POST', FALSE), + 'user_name' => $this->retrieve('receiver_email', 'String', FALSE), 'payment_processor_type_id' => CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal_Standard', 'id', 'name'), 'is_test' => empty($input['is_test']) ? 0 : 1, ); + $processorInfo = array(); if (!CRM_Financial_BAO_PaymentProcessor::retrieve($processorParams, $processorInfo)) { return FALSE; @@ -365,11 +378,11 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { return FALSE; } - $input['txnType'] = self::retrieve('txn_type', 'String', 'POST', FALSE); - $input['paymentStatus'] = self::retrieve('payment_status', 'String', 'POST', FALSE); - $input['invoice'] = self::retrieve('invoice', 'String', 'POST', TRUE); - $input['amount'] = self::retrieve('mc_gross', 'Money', 'POST', FALSE); - $input['reasonCode'] = self::retrieve('ReasonCode', 'String', 'POST', FALSE); + $input['txnType'] = $this->retrieve('txn_type', 'String', FALSE); + $input['paymentStatus'] = $this->retrieve('payment_status', 'String', FALSE); + $input['invoice'] = $this->retrieve('invoice', 'String', TRUE); + $input['amount'] = $this->retrieve('mc_gross', 'Money', FALSE); + $input['reasonCode'] = $this->retrieve('ReasonCode', 'String', FALSE); $billingID = $ids['billing']; $lookup = array( @@ -382,14 +395,14 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { "country-{$billingID}" => 'address_country_code', ); foreach ($lookup as $name => $paypalName) { - $value = self::retrieve($paypalName, 'String', 'POST', FALSE); + $value = $this->retrieve($paypalName, 'String', FALSE); $input[$name] = $value ? $value : NULL; } - $input['is_test'] = self::retrieve('test_ipn', 'Integer', 'POST', FALSE); - $input['fee_amount'] = self::retrieve('mc_fee', 'Money', 'POST', FALSE); - $input['net_amount'] = self::retrieve('settle_amount', 'Money', 'POST', FALSE); - $input['trxn_id'] = self::retrieve('txn_id', 'String', 'POST', FALSE); + $input['is_test'] = $this->retrieve('test_ipn', 'Integer', FALSE); + $input['fee_amount'] = $this->retrieve('mc_fee', 'Money', FALSE); + $input['net_amount'] = $this->retrieve('settle_amount', 'Money', FALSE); + $input['trxn_id'] = $this->retrieve('txn_id', 'String', FALSE); } } diff --git a/extern/ipn.php b/extern/ipn.php index 3032475228..3974cdc392 100644 --- a/extern/ipn.php +++ b/extern/ipn.php @@ -48,7 +48,7 @@ if (empty($_GET)) { } else { $log->alert('payment_notification PayPal_Standard', $_REQUEST); - $paypalIPN = new CRM_Core_Payment_PayPalIPN(); + $paypalIPN = new CRM_Core_Payment_PayPalIPN($_REQUEST); // @todo upgrade standard per Pro } try { diff --git a/tests/phpunit/CRM/Core/Payment/PayPalPNTest.php b/tests/phpunit/CRM/Core/Payment/PayPalPNTest.php new file mode 100644 index 0000000000..fb2fe90806 --- /dev/null +++ b/tests/phpunit/CRM/Core/Payment/PayPalPNTest.php @@ -0,0 +1,167 @@ +_paymentProcessorID = $this->paymentProcessorCreate(array('is_test' => 0, 'payment_processor_type_id' => 'PayPal_Standard')); + $this->_contactID = $this->individualCreate(); + $contributionPage = $this->callAPISuccess('contribution_page', 'create', array( + 'title' => "Test Contribution Page", + 'financial_type_id' => $this->_financialTypeID, + 'currency' => 'USD', + 'payment_processor' => $this->_paymentProcessorID, + ) + ); + $this->_contributionPageID = $contributionPage['id']; + } + + /** + * Tear down function. + */ + public function tearDown() { + $this->quickCleanUpFinancialEntities(); + } + + /** + * Test IPN response updates contribution_recur & contribution for first & second contribution. + * + * The scenario is that a pending contribution exists and the first call will update it to completed. + * The second will create a new contribution. + */ + public function testIPNPaymentRecurSuccess() { + $this->setupRecurringPaymentProcessorTransaction(); + $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction()); + $paypalIPN->main(); + $contribution = $this->callAPISuccess('contribution', 'getsingle', array('id' => $this->_contributionID)); + $this->assertEquals(1, $contribution['contribution_status_id']); + $this->assertEquals('8XA571746W2698126', $contribution['trxn_id']); + // source gets set by processor + $this->assertTrue(substr($contribution['contribution_source'], 0, 20) == "Online Contribution:"); + $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', array('id' => $this->_contributionRecurID)); + $this->assertEquals(5, $contributionRecur['contribution_status_id']); + $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurSubsequentTransaction()); + $paypalIPN->main(); + $contribution = $this->callAPISuccess('contribution', 'get', array( + 'contribution_recur_id' => $this->_contributionRecurID, + 'sequential' => 1, + )); + $this->assertEquals(2, $contribution['count']); + $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']); + } + + /** + * Test IPN response updates contribution_recur & contribution for first & second contribution. + */ + public function testIPNPaymentMembershipRecurSuccess() { + $this->setupMembershipRecurringPaymentProcessorTransaction(); + $this->callAPISuccessGetSingle('membership_payment', array()); + $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction()); + $paypalIPN->main(); + $contribution = $this->callAPISuccess('contribution', 'getsingle', array('id' => $this->_contributionID)); + $membershipEndDate = $this->callAPISuccessGetValue('membership', array('return' => 'end_date')); + $this->assertEquals(1, $contribution['contribution_status_id']); + $this->assertEquals('8XA571746W2698126', $contribution['trxn_id']); + // source gets set by processor + $this->assertTrue(substr($contribution['contribution_source'], 0, 20) == "Online Contribution:"); + $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', array('id' => $this->_contributionRecurID)); + $this->assertEquals(5, $contributionRecur['contribution_status_id']); + $paypalIPN = new CRM_Core_Payment_PaypalIPN($this->getPaypalRecurSubsequentTransaction()); + $paypalIPN->main(); + $this->assertEquals(strtotime('+ 1 year', strtotime($membershipEndDate)), strtotime($this->callAPISuccessGetValue('membership', array('return' => 'end_date')))); + $contribution = $this->callAPISuccess('contribution', 'get', array( + 'contribution_recur_id' => $this->_contributionRecurID, + 'sequential' => 1, + )); + $this->assertEquals(2, $contribution['count']); + $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']); + $this->callAPISuccessGetCount('line_item', array( + 'entity_id' => $this->ids['membership'], + 'entity_table' => 'civicrm_membership', + ), 2); + $this->callAPISuccessGetSingle('line_item', array( + 'contribution_id' => $contribution['values'][1]['id'], + 'entity_table' => 'civicrm_membership', + )); + $this->callAPISuccessGetSingle('membership_payment', array('contribution_id' => $contribution['values'][1]['id'])); + + } + + /** + * Get IPN style details for an incoming recurring transaction. + */ + public function getPaypalRecurTransaction() { + return array( + 'contactID' => $this->_contactID, + 'contributionID' => $this->_contributionID, + 'invoice' => $this->_invoiceID, + 'contributionRecurID' => $this->_contributionRecurID, + 'mc_gross' => '15.00', + 'module' => 'contribute', + 'payer_id' => '4NHUTA7ZUE92C', + 'payment_status' => 'Completed', + 'receiver_email' => 'sunil._1183377782_biz_api1.webaccess.co.in', + 'txn_type' => 'subscr_payment', + 'last_name' => 'Roberty', + 'payment_fee' => '0.63', + 'first_name' => 'Robert', + 'txn_id' => '8XA571746W2698126', + 'residence_country' => 'US', + ); + } + + /** + * Get IPN-style details for a second incoming transaction. + * + * @return array + */ + public function getPaypalRecurSubsequentTransaction() { + return array_merge($this->getPaypalRecurTransaction(), array('txn_id' => 'secondone')); + } + +} -- 2.25.1