From 319c265146982b8dadccc8c4a6d7a49fe4c28a13 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Mon, 26 Jun 2017 15:09:55 -0400 Subject: [PATCH 01/30] updating to latest generated code from civix. --- stripe.civix.php | 114 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/stripe.civix.php b/stripe.civix.php index 5eaebca..cc0d023 100644 --- a/stripe.civix.php +++ b/stripe.civix.php @@ -19,14 +19,14 @@ function _stripe_civix_civicrm_config(&$config = NULL) { $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR; $extDir = $extRoot . 'templates'; - if ( is_array( $template->template_dir ) ) { - array_unshift( $template->template_dir, $extDir ); + if (is_array($template->template_dir)) { + array_unshift($template->template_dir, $extDir); } else { - $template->template_dir = array( $extDir, $template->template_dir ); + $template->template_dir = array($extDir, $template->template_dir); } - $include_path = $extRoot . PATH_SEPARATOR . get_include_path( ); + $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); set_include_path($include_path); } @@ -55,6 +55,20 @@ function _stripe_civix_civicrm_install() { } } +/** + * Implements hook_civicrm_postInstall(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall + */ +function _stripe_civix_civicrm_postInstall() { + _stripe_civix_civicrm_config(); + if ($upgrader = _stripe_civix_upgrader()) { + if (is_callable(array($upgrader, 'onPostInstall'))) { + $upgrader->onPostInstall(); + } + } +} + /** * Implements hook_civicrm_uninstall(). * @@ -117,7 +131,7 @@ function _stripe_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { * @return CRM_Stripe_Upgrader */ function _stripe_civix_upgrader() { - if (!file_exists(__DIR__.'/CRM/Stripe/Upgrader.php')) { + if (!file_exists(__DIR__ . '/CRM/Stripe/Upgrader.php')) { return NULL; } else { @@ -153,7 +167,8 @@ function _stripe_civix_find_files($dir, $pattern) { while (FALSE !== ($entry = readdir($dh))) { $path = $subdir . DIRECTORY_SEPARATOR . $entry; if ($entry{0} == '.') { - } elseif (is_dir($path)) { + } + elseif (is_dir($path)) { $todos[] = $path; } } @@ -178,6 +193,9 @@ function _stripe_civix_civicrm_managed(&$entities) { $e['module'] = 'com.drastikbydesign.stripe'; } $entities[] = $e; + if (empty($e['params']['version'])) { + $e['params']['version'] = '3'; + } } } } @@ -212,14 +230,14 @@ function _stripe_civix_civicrm_caseTypes(&$caseTypes) { } /** -* (Delegated) Implements hook_civicrm_angularModules(). -* -* Find any and return any files matching "ang/*.ang.php" -* -* Note: This hook only runs in CiviCRM 4.5+. -* -* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules -*/ + * (Delegated) Implements hook_civicrm_angularModules(). + * + * Find any and return any files matching "ang/*.ang.php" + * + * Note: This hook only runs in CiviCRM 4.5+. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules + */ function _stripe_civix_civicrm_angularModules(&$angularModules) { if (!is_dir(__DIR__ . '/ang')) { return; @@ -259,33 +277,28 @@ function _stripe_civix_glob($pattern) { * @param array $menu - menu hierarchy * @param string $path - path where insertion should happen (ie. Administer/System Settings) * @param array $item - menu you need to insert (parent/child attributes will be filled for you) - * @param int $parentId - used internally to recurse in the menu structure */ -function _stripe_civix_insert_navigation_menu(&$menu, $path, $item, $parentId = NULL) { - static $navId; - +function _stripe_civix_insert_navigation_menu(&$menu, $path, $item) { // If we are done going down the path, insert menu if (empty($path)) { - if (!$navId) $navId = CRM_Core_DAO::singleValueQuery("SELECT max(id) FROM civicrm_navigation"); - $navId ++; - $menu[$navId] = array ( - 'attributes' => array_merge($item, array( + $menu[] = array( + 'attributes' => array_merge(array( 'label' => CRM_Utils_Array::value('name', $item), 'active' => 1, - 'parentID' => $parentId, - 'navID' => $navId, - )) + ), $item), ); - return true; + return TRUE; } else { // Find an recurse into the next level down - $found = false; + $found = FALSE; $path = explode('/', $path); $first = array_shift($path); foreach ($menu as $key => &$entry) { if ($entry['attributes']['name'] == $first) { - if (!$entry['child']) $entry['child'] = array(); + if (!isset($entry['child'])) { + $entry['child'] = array(); + } $found = _stripe_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item, $key); } } @@ -293,6 +306,49 @@ function _stripe_civix_insert_navigation_menu(&$menu, $path, $item, $parentId = } } +/** + * (Delegated) Implements hook_civicrm_navigationMenu(). + */ +function _stripe_civix_navigationMenu(&$nodes) { + if (!is_callable(array('CRM_Core_BAO_Navigation', 'fixNavigationMenu'))) { + _stripe_civix_fixNavigationMenu($nodes); + } +} + +/** + * Given a navigation menu, generate navIDs for any items which are + * missing them. + */ +function _stripe_civix_fixNavigationMenu(&$nodes) { + $maxNavID = 1; + array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) { + if ($key === 'navID') { + $maxNavID = max($maxNavID, $item); + } + }); + _stripe_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL); +} + +function _stripe_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { + $origKeys = array_keys($nodes); + foreach ($origKeys as $origKey) { + if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) { + $nodes[$origKey]['attributes']['parentID'] = $parentID; + } + // If no navID, then assign navID and fix key. + if (!isset($nodes[$origKey]['attributes']['navID'])) { + $newKey = ++$maxNavID; + $nodes[$origKey]['attributes']['navID'] = $newKey; + $nodes[$newKey] = $nodes[$origKey]; + unset($nodes[$origKey]); + $origKey = $newKey; + } + if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) { + _stripe_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']); + } + } +} + /** * (Delegated) Implements hook_civicrm_alterSettingsFolders(). * @@ -306,7 +362,7 @@ function _stripe_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { $configured = TRUE; $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; - if(is_dir($settingsDir) && !in_array($settingsDir, $metaDataFolders)) { + if (is_dir($settingsDir) && !in_array($settingsDir, $metaDataFolders)) { $metaDataFolders[] = $settingsDir; } } From 1aaee16315f8dc6cd88a5cae72055c2e9266836e Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Mon, 26 Jun 2017 15:11:03 -0400 Subject: [PATCH 02/30] adding basic api and first test. --- CRM/Stripe/Page/Webhook.php | 11 +- api/v3/Stripe/.Ipn.php.swp | Bin 0 -> 12288 bytes api/v3/Stripe/Ipn.php | 43 +++++ api/v3/Stripe/Listevents.php | 225 ++++++++++++++++++++++++ phpunit.xml.dist | 18 ++ tests/phpunit/CRM/Stripe/BaseTest.php | 128 ++++++++++++++ tests/phpunit/CRM/Stripe/DirectTest.php | 116 ++++++++++++ tests/phpunit/CRM/Stripe/IpnTest.php | 95 ++++++++++ tests/phpunit/bootstrap.php | 49 ++++++ 9 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 api/v3/Stripe/.Ipn.php.swp create mode 100644 api/v3/Stripe/Ipn.php create mode 100644 api/v3/Stripe/Listevents.php create mode 100644 phpunit.xml.dist create mode 100644 tests/phpunit/CRM/Stripe/BaseTest.php create mode 100644 tests/phpunit/CRM/Stripe/DirectTest.php create mode 100644 tests/phpunit/CRM/Stripe/IpnTest.php create mode 100644 tests/phpunit/bootstrap.php diff --git a/CRM/Stripe/Page/Webhook.php b/CRM/Stripe/Page/Webhook.php index 7eda0ea..b389746 100644 --- a/CRM/Stripe/Page/Webhook.php +++ b/CRM/Stripe/Page/Webhook.php @@ -7,7 +7,7 @@ require_once 'CRM/Core/Page.php'; class CRM_Stripe_Page_Webhook extends CRM_Core_Page { - function run() { + function run($data = null) { function getRecurInfo($subscription_id,$test_mode) { $query_params = array( @@ -76,8 +76,11 @@ function getRecurInfo($subscription_id,$test_mode) { return $recurring_info; } // Get the data from Stripe. - $data_raw = file_get_contents("php://input"); - $data = json_decode($data_raw); + + if (is_null($data)) { + $data_raw = file_get_contents("php://input"); + $data = json_decode($data_raw); + } if (!$data) { header('HTTP/1.1 406 Not acceptable'); CRM_Core_Error::Fatal("Stripe Callback: cannot json_decode data, exiting.
$data"); @@ -118,8 +121,10 @@ function getRecurInfo($subscription_id,$test_mode) { // Retrieve Event from Stripe using ID even though we already have the values now. // This is for extra security precautions mentioned here: https://stripe.com/docs/webhooks $stripe_event_data = \Stripe\Event::retrieve($data->id); + $customer_id = $stripe_event_data->data->object->customer; + switch($stripe_event_data->type) { // Successful recurring payment. case 'invoice.payment_succeeded': diff --git a/api/v3/Stripe/.Ipn.php.swp b/api/v3/Stripe/.Ipn.php.swp new file mode 100644 index 0000000000000000000000000000000000000000..62cd5f9efa8c0bf275d1d7f9429f6fd0033eb031 GIT binary patch literal 12288 zcmeI2O>Z1U5QaP42qYjlaVs2)+4aYE*6T!I6N6=pWMMld{y@lzM(vqiZ`-poLwC>G zs|X??0gA*Gi5tHGiC@4G4*UuvIB)@hgipNlvEFPPVz?zyul#DZXR4~ZpRO8d<=VTt zv$967H?A^#USzEM%KIO_;ay>uiLnFGRWiSS&c|l@Q1;I;!sAbhfIQX7feftSsZDvH zdMeP}=}`6t-v`&}y-Y>np|ZUA!n^(4m>XrK;h}7cEV8_DZRr zg(KhyI0BAaKmsCU?AvD<>tDp;@&Et9@Bfn*82cOg6Z!%A8u|)ahZdk;FEjQt z^c{2$`U3hK`V>l`F0>6bpo`Eiml*pA`VsmDilI$t8ajENvG1WTq0gWd=#S?Z`yILm z?LtlH1Z(^Y`VG1Z-GTCTA4Bfr2si?cfFs}tI0BAWLtB2JrLfXBS&)aSjh8k%BFdyGVi1iV z$-PdJ98NFjEUt~GLW{21^@eNlZa-j2H;N4|-CFa@?o)pQd-6Xhx)|}JIix4 zvdhzGW1C4=QZe6HeLi=(`Q8*&&;%J3??=`1{}FWdsnrwL8Fen`c&ae)S5l()tVjg+Uh6`w|0{0p~L~Unrd3qx$TDCL$ zXkZ>BUv5hi9HO_2dNQNMR6HwA3~8^ASD7kKi~ F{{u|HCGY?M literal 0 HcmV?d00001 diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php new file mode 100644 index 0000000..f2ba58f --- /dev/null +++ b/api/v3/Stripe/Ipn.php @@ -0,0 +1,43 @@ + array('id' => 12, 'name' => 'Twelve'), + 34 => array('id' => 34, 'name' => 'Thirty four'), + 56 => array('id' => 56, 'name' => 'Fifty six'), + ); + // ALTERNATIVE: $returnValues = array(); // OK, success + // ALTERNATIVE: $returnValues = array("Some value"); // OK, return a single value + + // Spec: civicrm_api3_create_success($values = 1, $params = array(), $entity = NULL, $action = NULL) + $webhook = new CRM_Stripe_Page_Webhook(); + $webhook->run($params['json_input']); + return civicrm_api3_create_success($returnValues); + } + else { + throw new API_Exception(/*errorMessage*/ 'Please include the json_input to process', /*errorCode*/ 1234); + } +} diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php new file mode 100644 index 0000000..39e7d36 --- /dev/null +++ b/api/v3/Stripe/Listevents.php @@ -0,0 +1,225 @@ + $$id); + } + else { + // By default, select the live stripe processor (we expect there to be + // only one). + $query_params = array('class_name' => 'Payment_Stripe', 'is_test' => 0); + } + try { + $results = civicrm_api3('PaymentProcessor', 'getsingle', $params); + // YES! I know, password and user are backwards. wtf?? + $sk = $results['user_name']; + $pk = $results['password']; + } + catch (CiviCRM_API3_Exception $e) { + if(preg_match('/Expected one PaymentProcessor but/', $e->getMessage())) { + throw new API_Exception(/*errorMessage*/ "Expected one live Stripe payment processor, but found none or more than one. Please specify id= OR pk= and sk=.", /*errorCode*/ 1234); + } + else { + throw new API_Exception(/*errorMessage*/ "Error getting the Stripe Payment Processor to use", /*errorCode*/ 1235); + } + } + } + + // Check to see if we should filter by type. + if (array_key_exists('type', $params) ) { + // Validate - since we will be appending this to an URL. + if (!civicrm_api3_stripe_VerifyEventType($params['type'])) { + throw new API_Exception(/*errorMessage*/ "Unrecognized Event Type.", /*errorCode*/ 1236); + } + else { + $type = $params['type']; + } + } + + // Created can only be passed in as an array + if (array_key_exists('created', $params)) { + $created = $params['created']; + if (!is_array($created)) { + throw new API_Exception(/*errorMessage*/ "Created can only be passed in programatically as an array", /*errorCode*/ 1237); + } + } + return array('sk' => $sk, 'pk' => $pk, 'type' => $type, 'created' => $created); +} + +/** + * Stripe.ListEvents API + * + * @param array $params + * @return array API result descriptor + * @see civicrm_api3_create_success + * @see civicrm_api3_create_error + * @throws API_Exception + */ +function civicrm_api3_stripe_Listevents($params) { + $parsed = civicrm_api3_stripe_ProcessParams($params); + $sk = $parsed['sk']; + $pk = $parsed['pk']; + $type = $parsed['type']; + $created = $parsed['created']; + + $args = array(); + if ($type) { + $args['type'] = $type; + } + if ($created) { + $args['created'] = $created; + } + + require_once ("packages/stripe-php/init.php"); + \Stripe\Stripe::setApiKey($sk); + $data_list = \Stripe\Event::all($args); + if (array_key_exists('error', $data_list)) { + $err = $data_list['error']; + throw new API_Exception(/*errorMessage*/ "Stripe returned an error: " . $err->message, /*errorCode*/ $err->type); + } + return civicrm_api3_create_success($data_list, $params); +} + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0f9f25d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + ./tests/phpunit + + + + + ./ + + + + + + + + diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php new file mode 100644 index 0000000..cec245e --- /dev/null +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -0,0 +1,128 @@ +installMe(__DIR__) + ->apply(); + } + + public function setUp() { + parent::setUp(); + $this->createPaymentProcessor(); + $this->createContact(); + $this->createContributionPage(); + $this->_created_ts = time(); + } + + public function tearDown() { + parent::tearDown(); + } + + /** + * Create contact. + */ + function createContact() { + $this->contact = $this->contact = \CRM_Core_DAO::createTestObject( + 'CRM_Contact_DAO_Contact', + array('contact_type' => 'Individual',) + ); + $this->_contactID = $this->contact->id; + $this->contact = $this->contact; + + // Now we have to add an email address. + $email = 'susie@example.org'; + civicrm_api3('email', 'create', array( + 'contact_id' => $this->_contactID, + 'email' => $email, + 'location_type_id' => 1 + )); + $this->contact->email = $email; + + + } + + /** + * Create a stripe payment processor. + * + */ + function createPaymentProcessor($params = array()) { + $params = array_merge(array( + 'name' => 'Stripe', + 'domain_id' => CRM_Core_Config::domainID(), + 'payment_processor_type_id' => 'Stripe', + 'title' => 'Stripe', + 'is_active' => 1, + 'is_default' => 0, + 'is_test' => 1, + 'is_recur' => 1, + 'user_name' => $this->_sk, + 'password' => $this->_pk, + 'url_site' => 'https://api.stripe.com/v1', + 'url_recur' => 'https://api.stripe.com/v1', + 'class_name' => 'Payment_Stripe', + 'billing_mode' => 1 + ), $params); + $result = civicrm_api3('PaymentProcessor', 'create', $params); + $this->assertEquals(0, $result['is_error']); + $this->_paymentProcessor = array_pop($result['values']); + $this->_paymentProcessorID = $result['id']; + } + /** + * Create a stripe contribution page. + * + */ + function createContributionPage($params = array()) { + $params = array_merge(array( + 'title' => "Test Contribution Page", + 'financial_type_id' => $this->_financialTypeID, + 'currency' => 'USD', + 'payment_processor' => $this->_paymentProcessorID, + 'max_amount' => 1000, + 'receipt_from_email' => 'gaia@the.cosmos', + 'receipt_from_name' => 'Pachamama', + 'is_email_receipt' => TRUE, + ), $params); + $result = civicrm_api3('ContributionPage', 'create', $params); + $this->assertEquals(0, $result['is_error']); + $this->_contributionPageID = $result['id']; + } + + +} diff --git a/tests/phpunit/CRM/Stripe/DirectTest.php b/tests/phpunit/CRM/Stripe/DirectTest.php new file mode 100644 index 0000000..9500f5e --- /dev/null +++ b/tests/phpunit/CRM/Stripe/DirectTest.php @@ -0,0 +1,116 @@ +installMe(__DIR__) + ->apply(); + } + + public function setUp() { + parent::setUp(); + } + + public function tearDown() { + parent::tearDown(); + } + + /** + * Test making a recurring contribution. + */ + public function testDirectSuccess() { + $this->setupTransaction(); + $this->doPayment(); + require_once('stripe-php/init.php'); + \Stripe\Stripe::setApiKey($this->_sk); + $found = FALSE; + try { + $results = \Stripe\Charge::retrieve(array( "id" => $this->_trxn_id)); + $found = TRUE; + } + catch (Stripe_Error $e) { + $found = FALSE; + } + + $this->assertTrue($found, 'Direct payment succeeded'); + } + + /** + * Submit to stripe + */ + public function doPayment() { + $mode = 'test'; + $pp = $this->_paymentProcessor; + $stripe = new CRM_Core_Payment_Stripe($mode, $pp); + $params = array( + 'amount' => $this->_total, + 'stripe_token' => array( + 'number' => '4111111111111111', + 'exp_month' => '12', + 'exp_year' => date('Y') + 1, + 'cvc' => '123', + 'name' => $this->contact->display_name, + 'address_line1' => '123 4th Street', + 'address_state' => 'NY', + 'address_zip' => '12345', + ), + 'email' => $this->contact->email, + 'description' => 'Test from Stripe Test Code', + 'currencyID' => 'USD', + 'invoiceID' => $this->_invoiceID, + ); + + $ret = $stripe->doDirectPayment($params); + $this->_trxn_id = $ret['trxn_id']; + $this->assertNotEmpty($this->_trxn_id, 'Received transaction id from Stripe'); + } + + /** + * Create contribition + */ + public function setupTransaction($params = array()) { + $contribution = civicrm_api3('contribution', 'create', array_merge(array( + 'contact_id' => $this->_contactID, + 'contribution_status_id' => 2, + 'payment_processor_id' => $this->_paymentProcessorID, + // processor provided ID - use contact ID as proxy. + 'processor_id' => $this->_contactID, + 'invoice_id' => $this->_invoiceID, + 'total_amount' => $this->_total, + 'invoice_id' => $this->_invoiceID, + 'financial_type_id' => 1, + 'contribution_status_id' => 'Pending', + 'contact_id' => $this->_contactID, + 'contribution_page_id' => $this->_contributionPageID, + 'payment_processor_id' => $this->_paymentProcessorID, + 'is_test' => 1, + ), $params)); + $this->assertEquals(0, $contribution['is_error']); + $this->_contributionID = $contribution['id']; + } +} diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php new file mode 100644 index 0000000..ba3db20 --- /dev/null +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -0,0 +1,95 @@ +installMe(__DIR__) + ->apply(); + } + + public function setUp() { + parent::setUp(); + } + + public function tearDown() { + parent::tearDown(); + } + + /** + * Test making a recurring contribution. + */ + public function testIPNRecurSuccess() { + $this->setupRecurringPaymentProcessorTransaction(); + + $params['sk'] = $this->_sk; + $params['created'] = array('gte' => $this->_created_ts - 86400); + // Now try to retrieve this transaction. + $transactions = civicrm_api3('Stripe', 'listevents', $params ); + // print_r($params); + // print_r($transactions); + return; + $stripe = new CRM_Stripe_Page_Webhook(); + $data = new stdClass(); + $data->id = $this->_invoiceID; + $data->livemode = FALSE; + $stripe->run($data); + $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $this->assertEquals(1, $contribution['contribution_status_id']); + } + + /** + * Create recurring contribition + */ + public function setupRecurringPaymentProcessorTransaction($params = array()) { + $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( + 'contact_id' => $this->_contactID, + 'amount' => 1000, + 'sequential' => 1, + 'installments' => 5, + 'frequency_unit' => 'Month', + 'frequency_interval' => 1, + 'invoice_id' => $this->_invoiceID, + 'contribution_status_id' => 2, + 'payment_processor_id' => $this->_paymentProcessorID, + // processor provided ID - use contact ID as proxy. + 'processor_id' => $this->_contactID, + 'api.contribution.create' => array( + 'total_amount' => '200', + 'invoice_id' => $this->_invoiceID, + 'financial_type_id' => 1, + 'contribution_status_id' => 'Pending', + 'contact_id' => $this->_contactID, + 'contribution_page_id' => $this->_contributionPageID, + 'payment_processor_id' => $this->_paymentProcessorID, + 'is_test' => 1, + ), + ), $params)); + $this->assertEquals(0, $contributionRecur['is_error']); + $this->_contributionRecurID = $contributionRecur['id']; + $this->_contributionID = $contributionRecur['values']['0']['api.contribution.create']['id']; + } +} diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 0000000..9de4be6 --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,49 @@ + array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR); + $oldOutput = getenv('CV_OUTPUT'); + putenv("CV_OUTPUT=json"); + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, 1); + + default: + throw new RuntimeException("Bad decoder format ($decode)"); + } +} From 10583e1ee9c84fd756165939b536f3d687c6b0e4 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 10:34:34 -0400 Subject: [PATCH 03/30] adding tests for Ipn/recurring payments. --- CRM/Stripe/Page/Webhook.php | 23 ++++--- tests/phpunit/CRM/Stripe/BaseTest.php | 85 ++++++++++++++++++++++++- tests/phpunit/CRM/Stripe/DirectTest.php | 67 +------------------ tests/phpunit/CRM/Stripe/IpnTest.php | 65 +++++++++++++------ 4 files changed, 145 insertions(+), 95 deletions(-) diff --git a/CRM/Stripe/Page/Webhook.php b/CRM/Stripe/Page/Webhook.php index b389746..4f2a63c 100644 --- a/CRM/Stripe/Page/Webhook.php +++ b/CRM/Stripe/Page/Webhook.php @@ -76,6 +76,11 @@ function getRecurInfo($subscription_id,$test_mode) { return $recurring_info; } // Get the data from Stripe. + $is_email_receipt = 1; + // Don't send emails while running php unit tests. + if (defined('STRIPE_PHPUNIT_TEST')) { + $is_email_receipt = 0; + } if (is_null($data)) { $data_raw = file_get_contents("php://input"); @@ -93,13 +98,15 @@ function getRecurInfo($subscription_id,$test_mode) { $processorId = CRM_Utils_Request::retrieve('ppid', 'Integer'); try { if (empty($processorId)) { - $stripe_key = civicrm_api3('PaymentProcessor', 'getvalue', array( - 'return' => 'user_name', + $processor_result = civicrm_api3('PaymentProcessor', 'get', array( + 'return' => array('user_name', 'id'), 'payment_processor_type_id' => 'Stripe', 'is_test' => $test_mode, 'is_active' => 1, 'options' => array('limit' => 1), )); + $processorId = $processor_result['id']; + $stripe_key = $processor_result['values'][$processorId]['user_name']; } else { $stripe_key = civicrm_api3('PaymentProcessor', 'getvalue', array( @@ -197,8 +204,9 @@ function getRecurInfo($subscription_id,$test_mode) { 'trxn_id' => $charge_id, 'total_amount' => $amount, 'fee_amount' => $fee, + 'payment_processor_id' => $processorId, + 'is_email_receipt' => $is_email_receipt, )); - return; } else { @@ -220,9 +228,8 @@ function getRecurInfo($subscription_id,$test_mode) { 'total_amount' => $amount, 'fee_amount' => $fee, //'invoice_id' => $new_invoice_id - contribution.repeattransaction doesn't support it currently - 'is_email_receipt' => 1, - )); - + 'is_email_receipt' => $is_email_receipt, + )); // Update invoice_id manually. repeattransaction doesn't return the new contrib id either, so we update the db. $query_params = array( 1 => array($new_invoice_id, 'String'), @@ -289,7 +296,7 @@ function getRecurInfo($subscription_id,$test_mode) { 'financial_type_id' => $recurring_info->financial_type_id, 'receive_date' => $fail_date, 'total_amount' => $amount, - 'is_email_receipt' => 1, + 'is_email_receipt' => $is_email_receipt, 'is_test' => $test_mode, )); @@ -304,7 +311,7 @@ function getRecurInfo($subscription_id,$test_mode) { 'financial_type_id' => $recurring_info->financial_type_id, 'receive_date' => $fail_date, 'total_amount' => $amount, - 'is_email_receipt' => 1, + 'is_email_receipt' => $is_email_receipt, 'is_test' => $test_mode, )); } diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index cec245e..361ec84 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -4,6 +4,8 @@ use Civi\Test\HookInterface; use Civi\Test\TransactionalInterface; +define('STRIPE_PHPUNIT_TEST', 1); + /** * FIXME - Add test description. * @@ -27,7 +29,9 @@ class CRM_Stripe_BaseTest extends \PHPUnit_Framework_TestCase implements Headles protected $_contributionPageID; protected $_paymentProcessorID; protected $_paymentProcessor; + protected $_trxn_id; protected $_created_ts; + protected $_subscriptionID; // Secret/public keys are PTP test keys. // protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; // protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; @@ -117,12 +121,89 @@ function createContributionPage($params = array()) { 'max_amount' => 1000, 'receipt_from_email' => 'gaia@the.cosmos', 'receipt_from_name' => 'Pachamama', - 'is_email_receipt' => TRUE, + 'is_email_receipt' => FALSE, ), $params); $result = civicrm_api3('ContributionPage', 'create', $params); $this->assertEquals(0, $result['is_error']); $this->_contributionPageID = $result['id']; } - + /** + * Submit to stripe + */ + public function doPayment($params = array()) { + $mode = 'test'; + $pp = $this->_paymentProcessor; + $stripe = new CRM_Core_Payment_Stripe($mode, $pp); + $params = array_merge(array( + 'payment_processor_id' => $this->_paymentProcessorID, + 'amount' => $this->_total, + 'stripe_token' => array( + 'number' => '4111111111111111', + 'exp_month' => '12', + 'exp_year' => date('Y') + 1, + 'cvc' => '123', + 'name' => $this->contact->display_name, + 'address_line1' => '123 4th Street', + 'address_state' => 'NY', + 'address_zip' => '12345', + ), + 'email' => $this->contact->email, + 'description' => 'Test from Stripe Test Code', + 'currencyID' => 'USD', + 'invoiceID' => $this->_invoiceID, + ), $params); + + $ret = $stripe->doDirectPayment($params); + if (array_key_exists('trxn_id', $ret)) { + $this->_trxn_id = $ret['trxn_id']; + } + if (array_key_exists('subscription_id', $ret)) { + $this->_subscriptionID = $ret['subscription_id']; + } + } + + /** + * Confirm that transaction id is legit and went through. + * + */ + public function assertValidTrxn() { + $this->assertNotEmpty($this->_trxn_id, "A trxn id was assigned"); + + require_once('stripe-php/init.php'); + \Stripe\Stripe::setApiKey($this->_sk); + $found = FALSE; + try { + $results = \Stripe\Charge::retrieve(array( "id" => $this->_trxn_id)); + $found = TRUE; + } + catch (Stripe_Error $e) { + $found = FALSE; + } + + $this->assertTrue($found, 'Assigned trxn_id is valid.'); + + } + /** + * Create contribition + */ + public function setupTransaction($params = array()) { + $contribution = civicrm_api3('contribution', 'create', array_merge(array( + 'contact_id' => $this->_contactID, + 'contribution_status_id' => 2, + 'payment_processor_id' => $this->_paymentProcessorID, + // processor provided ID - use contact ID as proxy. + 'processor_id' => $this->_contactID, + 'total_amount' => $this->_total, + 'invoice_id' => $this->_invoiceID, + 'financial_type_id' => $this->_financialTypeID, + 'contribution_status_id' => 'Pending', + 'contact_id' => $this->_contactID, + 'contribution_page_id' => $this->_contributionPageID, + 'payment_processor_id' => $this->_paymentProcessorID, + 'is_test' => 1, + ), $params)); + $this->assertEquals(0, $contribution['is_error']); + $this->_contributionID = $contribution['id']; + } } diff --git a/tests/phpunit/CRM/Stripe/DirectTest.php b/tests/phpunit/CRM/Stripe/DirectTest.php index 9500f5e..a30ac64 100644 --- a/tests/phpunit/CRM/Stripe/DirectTest.php +++ b/tests/phpunit/CRM/Stripe/DirectTest.php @@ -46,71 +46,8 @@ public function tearDown() { public function testDirectSuccess() { $this->setupTransaction(); $this->doPayment(); - require_once('stripe-php/init.php'); - \Stripe\Stripe::setApiKey($this->_sk); - $found = FALSE; - try { - $results = \Stripe\Charge::retrieve(array( "id" => $this->_trxn_id)); - $found = TRUE; - } - catch (Stripe_Error $e) { - $found = FALSE; - } - - $this->assertTrue($found, 'Direct payment succeeded'); + $this->assertValidTrxn(); } - /** - * Submit to stripe - */ - public function doPayment() { - $mode = 'test'; - $pp = $this->_paymentProcessor; - $stripe = new CRM_Core_Payment_Stripe($mode, $pp); - $params = array( - 'amount' => $this->_total, - 'stripe_token' => array( - 'number' => '4111111111111111', - 'exp_month' => '12', - 'exp_year' => date('Y') + 1, - 'cvc' => '123', - 'name' => $this->contact->display_name, - 'address_line1' => '123 4th Street', - 'address_state' => 'NY', - 'address_zip' => '12345', - ), - 'email' => $this->contact->email, - 'description' => 'Test from Stripe Test Code', - 'currencyID' => 'USD', - 'invoiceID' => $this->_invoiceID, - ); - - $ret = $stripe->doDirectPayment($params); - $this->_trxn_id = $ret['trxn_id']; - $this->assertNotEmpty($this->_trxn_id, 'Received transaction id from Stripe'); - } - - /** - * Create contribition - */ - public function setupTransaction($params = array()) { - $contribution = civicrm_api3('contribution', 'create', array_merge(array( - 'contact_id' => $this->_contactID, - 'contribution_status_id' => 2, - 'payment_processor_id' => $this->_paymentProcessorID, - // processor provided ID - use contact ID as proxy. - 'processor_id' => $this->_contactID, - 'invoice_id' => $this->_invoiceID, - 'total_amount' => $this->_total, - 'invoice_id' => $this->_invoiceID, - 'financial_type_id' => 1, - 'contribution_status_id' => 'Pending', - 'contact_id' => $this->_contactID, - 'contribution_page_id' => $this->_contributionPageID, - 'payment_processor_id' => $this->_paymentProcessorID, - 'is_test' => 1, - ), $params)); - $this->assertEquals(0, $contribution['is_error']); - $this->_contributionID = $contribution['id']; - } + } diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index ba3db20..a8ef0a3 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -20,8 +20,11 @@ */ require ('BaseTest.php'); class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { - + protected $_total = '200'; protected $_contributionRecurID; + protected $_installments = 5; + protected $_frequency_unit = 'month'; + protected $_frequency_interval = 1; public function setUpHeadless() { // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). @@ -43,44 +46,66 @@ public function tearDown() { * Test making a recurring contribution. */ public function testIPNRecurSuccess() { - $this->setupRecurringPaymentProcessorTransaction(); + $this->setupRecurringTransaction(); + $payment_extra_params = array( + 'is_recur' => 1, + 'contributionRecurID' => $this->_contributionRecurID, + 'frequency_unit' => $this->_frequency_unit, + 'frequency_interval' => $this->_frequency_interval, + 'installments' => $this->_installments + ); + $this->doPayment($payment_extra_params); + // Now check to see if an event was triggered and if so, process it. + // Get all events of the type invoice.payment_succeeded that have + // happened since this code was invoked. $params['sk'] = $this->_sk; - $params['created'] = array('gte' => $this->_created_ts - 86400); + $params['created'] = array('gte' => $this->_created_ts); + $params['type'] = 'invoice.payment_succeeded'; + // Now try to retrieve this transaction. $transactions = civicrm_api3('Stripe', 'listevents', $params ); - // print_r($params); - // print_r($transactions); - return; - $stripe = new CRM_Stripe_Page_Webhook(); - $data = new stdClass(); - $data->id = $this->_invoiceID; - $data->livemode = FALSE; - $stripe->run($data); - $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); - $this->assertEquals(1, $contribution['contribution_status_id']); + $payment_object = NULL; + foreach($transactions['values']['data'] as $transaction) { + if ($transaction->data->object->subscription == $this->_subscriptionID) { + // This is the one. + $payment_object = $transaction; + break; + } + } + + $contribution_status_id = NULL; + if ($payment_object) { + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($payment_object); + $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $contribution_status_id = $contribution['contribution_status_id']; + } + $this->assertEquals(1, $contribution_status_id, "Recurring payment was properly processed via a stripe event."); } /** * Create recurring contribition */ - public function setupRecurringPaymentProcessorTransaction($params = array()) { + public function setupRecurringTransaction($params = array()) { $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( + 'financial_type_id' => $this->_financialTypeID, + 'payment_instrument_id' => CRM_Core_OptionGroup::getValue('payment_instrument', 'Credit Card', 'name'), 'contact_id' => $this->_contactID, - 'amount' => 1000, + 'amount' => $this->_total, 'sequential' => 1, - 'installments' => 5, - 'frequency_unit' => 'Month', - 'frequency_interval' => 1, + 'installments' => $this->_installments, + 'frequency_unit' => $this->_frequency_unit, + 'frequency_interval' => $this->_frequency_interval, 'invoice_id' => $this->_invoiceID, 'contribution_status_id' => 2, 'payment_processor_id' => $this->_paymentProcessorID, // processor provided ID - use contact ID as proxy. 'processor_id' => $this->_contactID, 'api.contribution.create' => array( - 'total_amount' => '200', + 'total_amount' => $this->_total, 'invoice_id' => $this->_invoiceID, - 'financial_type_id' => 1, + 'financial_type_id' => $this->_financialTypeID, 'contribution_status_id' => 'Pending', 'contact_id' => $this->_contactID, 'contribution_page_id' => $this->_contributionPageID, From b4a6ae783a53db8ad38ca5d9560d0e7121775818 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 10:34:53 -0400 Subject: [PATCH 04/30] removing cache file accidentally committed. --- api/v3/Stripe/.Ipn.php.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 api/v3/Stripe/.Ipn.php.swp diff --git a/api/v3/Stripe/.Ipn.php.swp b/api/v3/Stripe/.Ipn.php.swp deleted file mode 100644 index 62cd5f9efa8c0bf275d1d7f9429f6fd0033eb031..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2O>Z1U5QaP42qYjlaVs2)+4aYE*6T!I6N6=pWMMld{y@lzM(vqiZ`-poLwC>G zs|X??0gA*Gi5tHGiC@4G4*UuvIB)@hgipNlvEFPPVz?zyul#DZXR4~ZpRO8d<=VTt zv$967H?A^#USzEM%KIO_;ay>uiLnFGRWiSS&c|l@Q1;I;!sAbhfIQX7feftSsZDvH zdMeP}=}`6t-v`&}y-Y>np|ZUA!n^(4m>XrK;h}7cEV8_DZRr zg(KhyI0BAaKmsCU?AvD<>tDp;@&Et9@Bfn*82cOg6Z!%A8u|)ahZdk;FEjQt z^c{2$`U3hK`V>l`F0>6bpo`Eiml*pA`VsmDilI$t8ajENvG1WTq0gWd=#S?Z`yILm z?LtlH1Z(^Y`VG1Z-GTCTA4Bfr2si?cfFs}tI0BAWLtB2JrLfXBS&)aSjh8k%BFdyGVi1iV z$-PdJ98NFjEUt~GLW{21^@eNlZa-j2H;N4|-CFa@?o)pQd-6Xhx)|}JIix4 zvdhzGW1C4=QZe6HeLi=(`Q8*&&;%J3??=`1{}FWdsnrwL8Fen`c&ae)S5l()tVjg+Uh6`w|0{0p~L~Unrd3qx$TDCL$ zXkZ>BUv5hi9HO_2dNQNMR6HwA3~8^ASD7kKi~ F{{u|HCGY?M From 2598090ab3ecfbbddd3da4e24d03d7260e89cd49 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 10:45:14 -0400 Subject: [PATCH 05/30] include the subscription_id so we can test to ensure it was successful. --- CRM/Core/Payment/Stripe.php | 1 + 1 file changed, 1 insertion(+) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 9ca4fab..e5007d5 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -820,6 +820,7 @@ public function doRecurPayment(&$params, $amount, $stripe_customer) { // Don't return a $params['trxn_id'] here or else recurring membership contribs will be set // "Completed" prematurely. Webhook.php does that. + $params['subscription_id'] = $subscription_id; return $params; } From d5d94609c8c6766f9092e49ea4702cee88c2ac07 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 10:45:37 -0400 Subject: [PATCH 06/30] avoid undefined index error. --- CRM/Core/Payment/Stripe.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index e5007d5..c0e3eee 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -203,6 +203,9 @@ public function stripeCatchErrors($op = 'create_customer', $stripe_params, $para $body = $e->getJsonBody(); $err = $body['error']; + if (!array_key_exists('code', $err)) { + $err['code'] = null; + } //$error_message .= 'Status is: ' . $e->getHttpStatus() . "
"; ////$error_message .= 'Param is: ' . $err['param'] . "
"; $error_message .= 'Type: ' . $err['type'] . "
"; From 50ec12886a85b188551bd19454779bea7ec2f139 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 11:14:00 -0400 Subject: [PATCH 07/30] fix nesting problem with run() and getRecurInfo() for some reason getRecurInfo() is nested within run() which prevents the page from being loaded twice (since getRecurInfo is defined twice). --- CRM/Stripe/Page/Webhook.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CRM/Stripe/Page/Webhook.php b/CRM/Stripe/Page/Webhook.php index 4f2a63c..21d4240 100644 --- a/CRM/Stripe/Page/Webhook.php +++ b/CRM/Stripe/Page/Webhook.php @@ -7,7 +7,6 @@ require_once 'CRM/Core/Page.php'; class CRM_Stripe_Page_Webhook extends CRM_Core_Page { - function run($data = null) { function getRecurInfo($subscription_id,$test_mode) { $query_params = array( @@ -75,6 +74,8 @@ function getRecurInfo($subscription_id,$test_mode) { return $recurring_info; } + + function run($data = null) { // Get the data from Stripe. $is_email_receipt = 1; // Don't send emails while running php unit tests. @@ -161,7 +162,7 @@ function getRecurInfo($subscription_id,$test_mode) { } // First, get the recurring contribution id and previous contribution id. - $recurring_info = getRecurInfo($subscription_id,$test_mode); + $recurring_info = self::getRecurInfo($subscription_id,$test_mode); // Fetch the previous contribution's status. $previous_contribution = civicrm_api3('Contribution', 'get', array( @@ -276,9 +277,9 @@ function getRecurInfo($subscription_id,$test_mode) { $transaction_id = $charge->id; // First, get the recurring contribution id and previous contribution id. - $recurring_info = getRecurInfo($subscription_id,$test_mode); - - // Fetch the previous contribution's status. + $recurring_info = self::getRecurInfo($subscription_id,$test_mode); + + // Fetch the previous contribution's status. $previous_contribution_status = civicrm_api3('Contribution', 'getvalue', array( 'sequential' => 1, 'return' => "contribution_status_id", @@ -341,7 +342,7 @@ function getRecurInfo($subscription_id,$test_mode) { $subscription_id = $stripe_event_data->data->object->id; // First, get the recurring contribution id and previous contribution id. - $recurring_info = getRecurInfo($subscription_id,$test_mode); + $recurring_info = self::getRecurInfo($subscription_id,$test_mode); //Cancel the recurring contribution $result = civicrm_api3('ContributionRecur', 'cancel', array( @@ -384,8 +385,8 @@ function getRecurInfo($subscription_id,$test_mode) { $new_civi_invoice = md5(uniqid(rand(), TRUE)); // First, get the recurring contribution id and previous contribution id. - $recurring_info = getRecurInfo($subscription_id,$test_mode); - + $recurring_info = self::getRecurInfo($subscription_id,$test_mode); + // Is there a pending charge due to a subcription change? Make up your mind!! $previous_contribution = civicrm_api3('Contribution', 'get', array( 'sequential' => 1, From ec7bf8dc9460712adc2d223bb7fe182037308cf6 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 11:14:52 -0400 Subject: [PATCH 08/30] test to ensure cancelling works. --- tests/phpunit/CRM/Stripe/BaseTest.php | 2 +- tests/phpunit/CRM/Stripe/IpnTest.php | 32 ++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 361ec84..70796df 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -49,6 +49,7 @@ public function setUpHeadless() { public function setUp() { parent::setUp(); + require_once('stripe-php/init.php'); $this->createPaymentProcessor(); $this->createContact(); $this->createContributionPage(); @@ -170,7 +171,6 @@ public function doPayment($params = array()) { public function assertValidTrxn() { $this->assertNotEmpty($this->_trxn_id, "A trxn id was assigned"); - require_once('stripe-php/init.php'); \Stripe\Stripe::setApiKey($this->_sk); $found = FALSE; try { diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index a8ef0a3..10b1504 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -74,14 +74,40 @@ public function testIPNRecurSuccess() { } } - $contribution_status_id = NULL; if ($payment_object) { $stripe = new CRM_Stripe_Page_Webhook(); $stripe->run($payment_object); - $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); - $contribution_status_id = $contribution['contribution_status_id']; } + $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $contribution_status_id = $contribution['contribution_status_id']; $this->assertEquals(1, $contribution_status_id, "Recurring payment was properly processed via a stripe event."); + + // Now, cancel the subscription and ensure it is properly cancelled. + \Stripe\Stripe::setApiKey($this->_sk); + $sub = \Stripe\Subscription::retrieve($this->_subscriptionID); + $sub->cancel(); + + $params['sk'] = $this->_sk; + $params['created'] = array('gte' => $this->_created_ts); + $params['type'] = 'customer.subscription.deleted'; + + // Now try to retrieve this transaction. + $transactions = civicrm_api3('Stripe', 'listevents', $params ); + $sub_object = NULL; + foreach($transactions['values']['data'] as $transaction) { + if ($transaction->data->object->id == $this->_subscriptionID) { + $sub_object = $transaction; + break; + } + } + if ($sub_object) { + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($sub_object); + } + $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); + $contribution_recur_status_id = $contribution_recur['contribution_status_id']; + $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_recur_status_id, 'name'); + $this->assertEquals('Cancelled', $status, "Recurring payment was properly cancelled via a stripe event."); } /** From b5108759889bd5612ed33fced3a051c41bb377f7 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 13:21:45 -0400 Subject: [PATCH 09/30] Don't run the parent: we don't want a page displayed. --- CRM/Stripe/Page/Webhook.php | 1 - api/v3/Stripe/Ipn.php | 60 +++++++++++----- api/v3/Stripe/Listevents.php | 100 ++++++++++++++------------- tests/phpunit/CRM/Stripe/IpnTest.php | 1 - 4 files changed, 93 insertions(+), 69 deletions(-) diff --git a/CRM/Stripe/Page/Webhook.php b/CRM/Stripe/Page/Webhook.php index 21d4240..61ad1ca 100644 --- a/CRM/Stripe/Page/Webhook.php +++ b/CRM/Stripe/Page/Webhook.php @@ -499,7 +499,6 @@ function run($data = null) { } - parent::run(); } } diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index f2ba58f..5b2a3d1 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -1,15 +1,28 @@ array('id' => 12, 'name' => 'Twelve'), - 34 => array('id' => 34, 'name' => 'Thirty four'), - 56 => array('id' => 56, 'name' => 'Fifty six'), - ); - // ALTERNATIVE: $returnValues = array(); // OK, success - // ALTERNATIVE: $returnValues = array("Some value"); // OK, return a single value - - // Spec: civicrm_api3_create_success($values = 1, $params = array(), $entity = NULL, $action = NULL) - $webhook = new CRM_Stripe_Page_Webhook(); - $webhook->run($params['json_input']); - return civicrm_api3_create_success($returnValues); + $object = NULL; + $ppid = NULL; + if (array_key_exists('id', $params)) { + $data = civicrm_api3('SystemLog', 'getsingle', array('id' => $params['id'], 'return' => array('message', 'context'))); + if (empty($data)) { + throw new API_Exception('Failed to find that entry in the system log', 3234); + } + $object = json_decode($data['context']); + if (preg_match('/processor_id=([0-9]+)$/', $object['message']), $matches) { + $ppid = $matches[1]; + } + else { + throw new API_Exception('Failed to find payment processor id in system log', 3235); + } } - else { - throw new API_Exception(/*errorMessage*/ 'Please include the json_input to process', /*errorCode*/ 1234); + elseif (array_key_exists('evtid', $params)) { + if (!array_key_exists('ppid', $params)) { + throw new API_Exception('Please pass the payment processor id (ppid) if using evtid.', 3235); + } + $object = new stdClass(); + $object->id = $params['evtid']; + $ppid = $params['ppid']; } + + $_REQUEST['ppid'] = $ppid; + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($object); } diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index 39e7d36..849f736 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -1,20 +1,26 @@ $$id); } else { - // Select the right payment processor to use. - if ($id) { - $query_params = array('id' => $$id); + // By default, select the live stripe processor (we expect there to be + // only one). + $query_params = array('class_name' => 'Payment_Stripe', 'is_test' => 0); + } + try { + $results = civicrm_api3('PaymentProcessor', 'getsingle', $params); + // YES! I know, password and user are backwards. wtf?? + $sk = $results['user_name']; + } + catch (CiviCRM_API3_Exception $e) { + if(preg_match('/Expected one PaymentProcessor but/', $e->getMessage())) { + throw new API_Exception(/*errorMessage*/ "Expected one live Stripe payment processor, but found none or more than one. Please specify id= OR pk= and sk=.", /*errorCode*/ 1234); } else { - // By default, select the live stripe processor (we expect there to be - // only one). - $query_params = array('class_name' => 'Payment_Stripe', 'is_test' => 0); - } - try { - $results = civicrm_api3('PaymentProcessor', 'getsingle', $params); - // YES! I know, password and user are backwards. wtf?? - $sk = $results['user_name']; - $pk = $results['password']; - } - catch (CiviCRM_API3_Exception $e) { - if(preg_match('/Expected one PaymentProcessor but/', $e->getMessage())) { - throw new API_Exception(/*errorMessage*/ "Expected one live Stripe payment processor, but found none or more than one. Please specify id= OR pk= and sk=.", /*errorCode*/ 1234); - } - else { - throw new API_Exception(/*errorMessage*/ "Error getting the Stripe Payment Processor to use", /*errorCode*/ 1235); - } + throw new API_Exception(/*errorMessage*/ "Error getting the Stripe Payment Processor to use", /*errorCode*/ 1235); } } @@ -185,7 +182,7 @@ function civicrm_api3_stripe_ProcessParams($params) { throw new API_Exception(/*errorMessage*/ "Created can only be passed in programatically as an array", /*errorCode*/ 1237); } } - return array('sk' => $sk, 'pk' => $pk, 'type' => $type, 'created' => $created); + return array('sk' => $sk, 'type' => $type, 'created' => $created, 'limit' => $limit, 'starting_after' => $starting_after); } /** @@ -200,9 +197,10 @@ function civicrm_api3_stripe_ProcessParams($params) { function civicrm_api3_stripe_Listevents($params) { $parsed = civicrm_api3_stripe_ProcessParams($params); $sk = $parsed['sk']; - $pk = $parsed['pk']; $type = $parsed['type']; $created = $parsed['created']; + $limit = $parsed['limit']; + $starting_after = $parsed['starting_after']; $args = array(); if ($type) { @@ -211,6 +209,12 @@ function civicrm_api3_stripe_Listevents($params) { if ($created) { $args['created'] = $created; } + if ($limit) { + $args['limit'] = $limit; + } + if ($starting_after) { + $args['starting_after'] = $starting_after; + } require_once ("packages/stripe-php/init.php"); \Stripe\Stripe::setApiKey($sk); @@ -219,7 +223,7 @@ function civicrm_api3_stripe_Listevents($params) { $err = $data_list['error']; throw new API_Exception(/*errorMessage*/ "Stripe returned an error: " . $err->message, /*errorCode*/ $err->type); } - return civicrm_api3_create_success($data_list, $params); + return civicrm_api3_create_success($data_list); } diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index 10b1504..5e07e8d 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -59,7 +59,6 @@ public function testIPNRecurSuccess() { // Now check to see if an event was triggered and if so, process it. // Get all events of the type invoice.payment_succeeded that have // happened since this code was invoked. - $params['sk'] = $this->_sk; $params['created'] = array('gte' => $this->_created_ts); $params['type'] = 'invoice.payment_succeeded'; From 979b343f0499775e0157f9df16eaa746019ba946 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 13:23:02 -0400 Subject: [PATCH 10/30] update test keys. --- tests/phpunit/CRM/Stripe/BaseTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 70796df..6642a02 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -33,11 +33,8 @@ class CRM_Stripe_BaseTest extends \PHPUnit_Framework_TestCase implements Headles protected $_created_ts; protected $_subscriptionID; // Secret/public keys are PTP test keys. - // protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; - // protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; - // MFPL Secret key - protected $_sk = 'sk_test_0f3Nja19AQvQvLczwI5lV021'; - protected $_pk = 'pk_test_4Q4VmBJAjn93vENmkka8YWSD'; + protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; + protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; public function setUpHeadless() { // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). From 2cfe23b0129c6a6c59f20acca2545da947eab86e Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 28 Jun 2017 13:23:24 -0400 Subject: [PATCH 11/30] new api command to populate the system log. --- api/v3/Stripe/Populatelog.php | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 api/v3/Stripe/Populatelog.php diff --git a/api/v3/Stripe/Populatelog.php b/api/v3/Stripe/Populatelog.php new file mode 100644 index 0000000..ef1be49 --- /dev/null +++ b/api/v3/Stripe/Populatelog.php @@ -0,0 +1,87 @@ + 'Payment_Stripe', 'is_test' => 0, 'return' => 'id'); + try { + $ppid = civicrm_api3('PaymentProcessor', 'getvalue', $params); + } + catch (CiviCRM_API3_Exception $e) { + throw new API_Exception("Expected one live Stripe payment processor, but found none or more than one. Please specify ppid=.", 2234); + } + } + + $params = array('limit' => 100, 'type' => 'invoice.payment_succeeded'); + if ($ppid) { + $params['ppid'] = $ppid; + } + + $items = array(); + $last_item = NULL; + $more = TRUE; + while(1) { + if ($last_item) { + $params['starting_after'] = $last_item->id; + } + $objects = civicrm_api3('Stripe', 'Listevents', $params); + + if (count($objects['values']['data']) == 0) { + // No more! + break; + } + $items = array_merge($items, $objects['values']['data']); + $last_item = end($objects['values']['data']); + } + $results = array(); + foreach($items as $item) { + $id = $item->id; + // Insert into System Log if it doesn't exist. + $like_event_id = '%event_id=' . addslashes($id); + $sql = "SELECT id FROM civicrm_system_log WHERE message LIKE '$like_event_id'"; + $dao= CRM_Core_DAO::executeQuery($sql); + if ($dao->N == 0) { + $message = "payment_notification processor_id=${ppid} event_id=${id}"; + $log = new CRM_Utils_SystemLogger(); + $log->alert($message, $item); + $results[] = $id; + } + } + return civicrm_api3_create_success($results); + +} From 28cefc5bd2b95aeb708318c4cf83bdbc4f9dfcfc Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 30 Jun 2017 10:54:49 -0400 Subject: [PATCH 12/30] minor refactoring to make testing and api easier Specifically adding api call to setup a test PP. --- api/v3/Stripe/Ipn.php | 15 +++++--- api/v3/Stripe/Listevents.php | 12 +++---- api/v3/Stripe/Setuptest.php | 52 +++++++++++++++++++++++++++ tests/phpunit/CRM/Stripe/BaseTest.php | 25 ++++--------- tests/phpunit/CRM/Stripe/IpnTest.php | 1 + 5 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 api/v3/Stripe/Setuptest.php diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index 5b2a3d1..d466378 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -43,7 +43,7 @@ function civicrm_api3_stripe_Ipn($params) { throw new API_Exception('Failed to find that entry in the system log', 3234); } $object = json_decode($data['context']); - if (preg_match('/processor_id=([0-9]+)$/', $object['message']), $matches) { + if (preg_match('/processor_id=([0-9]+)$/', $object['message'], $matches)) { $ppid = $matches[1]; } else { @@ -52,14 +52,21 @@ function civicrm_api3_stripe_Ipn($params) { } elseif (array_key_exists('evtid', $params)) { if (!array_key_exists('ppid', $params)) { - throw new API_Exception('Please pass the payment processor id (ppid) if using evtid.', 3235); + throw new API_Exception('Please pass the payment processor id (ppid) if using evtid.', 3236); } - $object = new stdClass(); - $object->id = $params['evtid']; $ppid = $params['ppid']; + $results = civicrm_api3('PaymentProcessor', 'getsingle', array('id' => $ppid)); + // YES! I know, password and user are backwards. wtf?? + $sk = $results['user_name']; + + require_once ("packages/stripe-php/init.php"); + \Stripe\Stripe::setApiKey($sk); + $object = \Stripe\Event::retrieve($params['evtid']); } $_REQUEST['ppid'] = $ppid; $stripe = new CRM_Stripe_Page_Webhook(); $stripe->run($object); + return civicrm_api3_create_success(array()); + } diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index 849f736..2927a72 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -143,7 +143,7 @@ function civicrm_api3_stripe_ProcessParams($params) { // Select the right payment processor to use. if ($ppid) { - $query_params = array('id' => $$id); + $query_params = array('id' => $ppid); } else { // By default, select the live stripe processor (we expect there to be @@ -151,16 +151,16 @@ function civicrm_api3_stripe_ProcessParams($params) { $query_params = array('class_name' => 'Payment_Stripe', 'is_test' => 0); } try { - $results = civicrm_api3('PaymentProcessor', 'getsingle', $params); + $results = civicrm_api3('PaymentProcessor', 'getsingle', $query_params); // YES! I know, password and user are backwards. wtf?? $sk = $results['user_name']; } catch (CiviCRM_API3_Exception $e) { if(preg_match('/Expected one PaymentProcessor but/', $e->getMessage())) { - throw new API_Exception(/*errorMessage*/ "Expected one live Stripe payment processor, but found none or more than one. Please specify id= OR pk= and sk=.", /*errorCode*/ 1234); + throw new API_Exception("Expected one live Stripe payment processor, but found none or more than one. Please specify ppid=.", 1234); } else { - throw new API_Exception(/*errorMessage*/ "Error getting the Stripe Payment Processor to use", /*errorCode*/ 1235); + throw new API_Exception("Error getting the Stripe Payment Processor to use", 1235); } } @@ -168,7 +168,7 @@ function civicrm_api3_stripe_ProcessParams($params) { if (array_key_exists('type', $params) ) { // Validate - since we will be appending this to an URL. if (!civicrm_api3_stripe_VerifyEventType($params['type'])) { - throw new API_Exception(/*errorMessage*/ "Unrecognized Event Type.", /*errorCode*/ 1236); + throw new API_Exception("Unrecognized Event Type.", 1236); } else { $type = $params['type']; @@ -179,7 +179,7 @@ function civicrm_api3_stripe_ProcessParams($params) { if (array_key_exists('created', $params)) { $created = $params['created']; if (!is_array($created)) { - throw new API_Exception(/*errorMessage*/ "Created can only be passed in programatically as an array", /*errorCode*/ 1237); + throw new API_Exception("Created can only be passed in programatically as an array", 1237); } } return array('sk' => $sk, 'type' => $type, 'created' => $created, 'limit' => $limit, 'starting_after' => $starting_after); diff --git a/api/v3/Stripe/Setuptest.php b/api/v3/Stripe/Setuptest.php new file mode 100644 index 0000000..3119fbe --- /dev/null +++ b/api/v3/Stripe/Setuptest.php @@ -0,0 +1,52 @@ + 'Stripe', + 'domain_id' => CRM_Core_Config::domainID(), + 'payment_processor_type_id' => 'Stripe', + 'title' => 'Stripe', + 'is_active' => 1, + 'is_default' => 0, + 'is_test' => 1, + 'is_recur' => 1, + 'user_name' => $params['sk'], + 'password' => $params['pk'], + 'url_site' => 'https://api.stripe.com/v1', + 'url_recur' => 'https://api.stripe.com/v1', + 'class_name' => 'Payment_Stripe', + 'billing_mode' => 1 + ); + $result = civicrm_api3('PaymentProcessor', 'create', $params); + return civicrm_api3_create_success($result['values']); +} diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 6642a02..9c50ad6 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -85,27 +85,14 @@ function createContact() { * */ function createPaymentProcessor($params = array()) { - $params = array_merge(array( - 'name' => 'Stripe', - 'domain_id' => CRM_Core_Config::domainID(), - 'payment_processor_type_id' => 'Stripe', - 'title' => 'Stripe', - 'is_active' => 1, - 'is_default' => 0, - 'is_test' => 1, - 'is_recur' => 1, - 'user_name' => $this->_sk, - 'password' => $this->_pk, - 'url_site' => 'https://api.stripe.com/v1', - 'url_recur' => 'https://api.stripe.com/v1', - 'class_name' => 'Payment_Stripe', - 'billing_mode' => 1 - ), $params); - $result = civicrm_api3('PaymentProcessor', 'create', $params); - $this->assertEquals(0, $result['is_error']); - $this->_paymentProcessor = array_pop($result['values']); + $result = civicrm_api3('Stripe', 'setuptest', $params); + $processor = array_pop($result['values']); + $this->_sk = $processor['user_name']; + $this->_pk = $processor['password']; + $this->_paymentProcessor = $processor; $this->_paymentProcessorID = $result['id']; } + /** * Create a stripe contribution page. * diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index 5e07e8d..3d81f93 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -61,6 +61,7 @@ public function testIPNRecurSuccess() { // happened since this code was invoked. $params['created'] = array('gte' => $this->_created_ts); $params['type'] = 'invoice.payment_succeeded'; + $params['ppid'] = $this->_paymentProcessorID; // Now try to retrieve this transaction. $transactions = civicrm_api3('Stripe', 'listevents', $params ); From 8cd513aa0e459a4d4adb261360b1567190fc5a94 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 30 Jun 2017 11:10:00 -0400 Subject: [PATCH 13/30] documenting API --- README.md | 18 ++++++++++++++++++ api/v3/Stripe/Listevents.php | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df41ab0..7df2202 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,24 @@ Inside the customer you will see a Subscriptions section. Click Cancel on the su Stripe.com will cancel the subscription and will send a webhook to your site (if you have set the webhook options correctly). Then the stripe_civicrm extension will process the webhook and cancel the Civi recurring contribution. +API +------------ +This extension comes with several APIs to help you troubleshoot problems. These can be run via /civicrm/api or via drush if you are using Drupal (drush cvapi Stripe.XXX). + +The api commands are: + + * Listevents: Events are the notifications that Stripe sends to the Webhook. Listevents will list all notifications that have been sent. You can further restrict them with the following parameters: + * ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one. + * type - Limit to the given Stripe events type. By default, show all. Optinally limit to, for example, invoice.payment_succeeded. + * limit - Limit number of results returned (100 is max, 10 is default). + * starting_after - Only return results after this event id. This can be used for paging purposes - if you want to retreive more than 100 results. + * Populatelog: If you are running a version of CiviCRM that supports the SystemLog - then this API call will populate your SystemLog with all of your past Stripe Events. You can safely re-run and not create duplicates. With a populated SystemLog - you can selectively replay events that may have caused errors the first time or otherwise not been properly recorded. Parameters: + * ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one. + * Ipn: Replay a given Stripe Event. Parameters. This will always fetch the chosen Event from Stripe before replaying. + * id - The id from the SystemLog of the event to replay. + * evtid - The Event ID as provided by Stripe. + * ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one. + GOOD TO KNOW ------------ * The stripe-php package has been added to this project & no longer needs to be diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index 2927a72..f1509c2 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -16,11 +16,11 @@ * @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards */ function _civicrm_api3_stripe_ListEvents_spec(&$spec) { - $spec['ppid']['title'] = ts("Payment Processor ID to use"); + $spec['ppid']['title'] = ts("Use the given Payment Processor ID"); $spec['ppid']['type'] = CRM_Utils_Type::T_INT; - $spec['type']['title'] = ts("The type of Stripe Events to limit to (default is all)."); - $spec['limit']['title'] = ts("Limit results to a specific number. 100 is the max, 10 is default."); - $spec['starting_after']['title'] = ts("Only get events after this event id."); + $spec['type']['title'] = ts("Limit to the given Stripe events type"); + $spec['limit']['title'] = ts("Limit number of results returned (100 is max)"); + $spec['starting_after']['title'] = ts("Only return results after this event id."); } /** From 4f44cd4c4097fa8d9a5165621986e56e832d2957 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 30 Jun 2017 11:14:08 -0400 Subject: [PATCH 14/30] adding documentation on tests. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7df2202..ad69e0e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,18 @@ The api commands are: * evtid - The Event ID as provided by Stripe. * ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one. +TESTS +------------ +This extension comes with two PHP Unit tests: + + * Ipn - This unit test ensures that a recurring contribution is properly updated after the event is received from Stripe and that it is properly canceled when cancelled via Stripe. + * Direct - This unit test ensures that a direct payment to Stripe is properly recorded in the database. + +Tests can be run most easily via an installation made through CiviCRM Buildkit (https://github.com/civicrm/civicrm-buildkit) by running: + + phpunit4 tests/phpunit/CRM/Stripe/IpnTest.php + phpunit4 tests/phpunit/CRM/Stripe/DirectTest.php + GOOD TO KNOW ------------ * The stripe-php package has been added to this project & no longer needs to be From 0a55de141998ee945504da000a64beebfced9d2b Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 30 Jun 2017 11:42:52 -0400 Subject: [PATCH 15/30] add contact_id this will make it easier to selectively replay --- api/v3/Stripe/Populatelog.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/v3/Stripe/Populatelog.php b/api/v3/Stripe/Populatelog.php index ef1be49..639a2b7 100644 --- a/api/v3/Stripe/Populatelog.php +++ b/api/v3/Stripe/Populatelog.php @@ -77,6 +77,10 @@ function civicrm_api3_stripe_Populatelog($params) { $dao= CRM_Core_DAO::executeQuery($sql); if ($dao->N == 0) { $message = "payment_notification processor_id=${ppid} event_id=${id}"; + $contact_id = civicrm_api3_stripe_cid_for_trxn($item->data->object->charge); + if ($contact_id) { + $item['contact_id'] = $contact_id; + } $log = new CRM_Utils_SystemLogger(); $log->alert($message, $item); $results[] = $id; @@ -85,3 +89,9 @@ function civicrm_api3_stripe_Populatelog($params) { return civicrm_api3_create_success($results); } + +function civcrm_api3_stripe_cid_for_trxn($trxn) { + $params = array('trxn_id' => $trxn, 'return' => 'contact_id'); + $result = civicrm_api3('Contribution', 'getvalue', $params); + return $result; +} From 8e2455ea934ffb0f2db13927da77f1cfa0675553 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 5 Jul 2017 21:06:23 -0400 Subject: [PATCH 16/30] Use new IPN class if it is available. https://github.com/drastik/com.drastikbydesign.stripe/issues/223 --- tests/phpunit/CRM/Stripe/IpnTest.php | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index 3d81f93..b71b450 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -75,8 +75,18 @@ public function testIPNRecurSuccess() { } if ($payment_object) { - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($payment_object); + if (class_exists('CRM_Core_Payment_StripeIPN')) { + // The $_GET['processor_id'] value is normally set by + // CRM_Core_Payment::handlePaymentMethod + $_GET['processor_id'] = $this->_paymentProcessorID; + $ipnClass = new CRM_Core_Payment_StripeIPN($payment_object); + $ipnClass->main(); + } + else { + // Deprecated method. + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($payment_object); + } } $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); $contribution_status_id = $contribution['contribution_status_id']; @@ -101,8 +111,18 @@ public function testIPNRecurSuccess() { } } if ($sub_object) { - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($sub_object); + if (class_exists('CRM_Core_Payment_StripeIPN')) { + // The $_GET['processor_id'] value is normally set by + // CRM_Core_Payment::handlePaymentMethod + $_GET['processor_id'] = $this->_paymentProcessorID; + $ipnClass = new CRM_Core_Payment_StripeIPN($sub_object); + $ipnClass->main(); + } + else { + // Deprecated method. + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($sub_object); + } } $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); $contribution_recur_status_id = $contribution_recur['contribution_status_id']; From 2e590d5990598e0cf436fe6bec5613e527f89d19 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 7 Jul 2017 17:01:13 -0400 Subject: [PATCH 17/30] test for failed recurring payments. --- CRM/Stripe/Page/Webhook.php | 15 +++++-- tests/phpunit/CRM/Stripe/BaseTest.php | 18 +++++++- tests/phpunit/CRM/Stripe/IpnTest.php | 63 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/CRM/Stripe/Page/Webhook.php b/CRM/Stripe/Page/Webhook.php index 61ad1ca..28d5b1b 100644 --- a/CRM/Stripe/Page/Webhook.php +++ b/CRM/Stripe/Page/Webhook.php @@ -126,13 +126,20 @@ function run($data = null) { \Stripe\Stripe::setAppInfo('CiviCRM', CRM_Utils_System::version(), CRM_Utils_System::baseURL()); \Stripe\Stripe::setApiKey($stripe_key); - // Retrieve Event from Stripe using ID even though we already have the values now. - // This is for extra security precautions mentioned here: https://stripe.com/docs/webhooks - $stripe_event_data = \Stripe\Event::retrieve($data->id); + if (defined('STRIPE_PHPUNIT_TEST') && $data->type == 'invoice.payment_failed') { + // It's impossible to fake a failed payment on a recurring + // contribution in an automated way, so we are faking it in + // unit test. + $stripe_event_data = $data; + } + else { + // Retrieve Event from Stripe using ID even though we already have the values now. + // This is for extra security precautions mentioned here: https://stripe.com/docs/webhooks + $stripe_event_data = \Stripe\Event::retrieve($data->id); + } $customer_id = $stripe_event_data->data->object->customer; - switch($stripe_event_data->type) { // Successful recurring payment. case 'invoice.payment_succeeded': diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 9c50ad6..6c8e627 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -35,6 +35,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit_Framework_TestCase implements Headles // Secret/public keys are PTP test keys. protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; + protected $_cc = NULL; public function setUpHeadless() { // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). @@ -51,6 +52,21 @@ public function setUp() { $this->createContact(); $this->createContributionPage(); $this->_created_ts = time(); + $this->set_cc(); + } + + /** + * Switch between test cc number that works and that fails + * + */ + public function set_cc($type = 'works') { + // See https://stripe.com/docs/testing + if ($type == 'works') { + $this->_cc = '4111111111111111'; + } + elseif ($type == 'fails') { + $this->_cc = '4000000000000002'; + } } public function tearDown() { @@ -124,7 +140,7 @@ public function doPayment($params = array()) { 'payment_processor_id' => $this->_paymentProcessorID, 'amount' => $this->_total, 'stripe_token' => array( - 'number' => '4111111111111111', + 'number' => $this->_cc, 'exp_month' => '12', 'exp_year' => date('Y') + 1, 'cvc' => '123', diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index b71b450..ee85e95 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -41,7 +41,70 @@ public function setUp() { public function tearDown() { parent::tearDown(); } + /** + * Test making a failed recurring contribution. + */ + public function testIPNRecurFail() { + $this->setupRecurringTransaction(); + $payment_extra_params = array( + 'is_recur' => 1, + 'contributionRecurID' => $this->_contributionRecurID, + 'frequency_unit' => $this->_frequency_unit, + 'frequency_interval' => $this->_frequency_interval, + 'installments' => $this->_installments + ); + // Note - this will succeed. It is very hard to test a failed transaction. + // We will manipulate the event to make it a failed transactin below. + $this->doPayment($payment_extra_params); + // Now check to see if an event was triggered and if so, process it. + // Get all events of the type invoice.payment_failed that have + // happened since this code was invoked. + $params['created'] = array('gte' => $this->_created_ts); + $params['type'] = 'invoice.payment_succeeded'; + $params['ppid'] = $this->_paymentProcessorID; + + // Now try to retrieve this transaction. + $transactions = civicrm_api3('Stripe', 'listevents', $params ); + $payment_object = NULL; + foreach($transactions['values']['data'] as $transaction) { + if ($transaction->data->object->subscription == $this->_subscriptionID) { + // This is the one. + $payment_object = $transaction; + break; + } + } + + if ($payment_object) { + // Now manipulate the transaction so it appears to be a failed one. + $payment_object->type = 'invoice.payment_failed'; + if (class_exists('CRM_Core_Payment_StripeIPN')) { + // The $_GET['processor_id'] value is normally set by + // CRM_Core_Payment::handlePaymentMethod + $_GET['processor_id'] = $this->_paymentProcessorID; + $ipnClass = new CRM_Core_Payment_StripeIPN($payment_object); + $ipnClass->main(); + } + else { + // Deprecated method. + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($payment_object); + } + } + + $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $contribution_status_id = $contribution['contribution_status_id']; + + $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_status_id, 'name'); + $this->assertEquals('Failed', $status, "Failed contribution was properly marked as failed via a stripe event."); + $failure_count = civicrm_api3('ContributionRecur', 'getvalue', array( + 'sequential' => 1, + 'id' => $this->_contributionRecurID, + 'return' => 'failure_count', + )); + $this->assertEquals(1, $failure_count, "Failed contribution count is correct.."); + + } /** * Test making a recurring contribution. */ From e6dba9743e16a4eae2fce98b28c410aade78caf3 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 7 Jul 2017 22:11:40 -0400 Subject: [PATCH 18/30] refactoring - reuse code when possible. --- tests/phpunit/CRM/Stripe/IpnTest.php | 128 ++++++++++----------------- 1 file changed, 49 insertions(+), 79 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index ee85e95..0ccc79e 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -58,38 +58,11 @@ public function testIPNRecurFail() { $this->doPayment($payment_extra_params); // Now check to see if an event was triggered and if so, process it. - // Get all events of the type invoice.payment_failed that have - // happened since this code was invoked. - $params['created'] = array('gte' => $this->_created_ts); - $params['type'] = 'invoice.payment_succeeded'; - $params['ppid'] = $this->_paymentProcessorID; - - // Now try to retrieve this transaction. - $transactions = civicrm_api3('Stripe', 'listevents', $params ); - $payment_object = NULL; - foreach($transactions['values']['data'] as $transaction) { - if ($transaction->data->object->subscription == $this->_subscriptionID) { - // This is the one. - $payment_object = $transaction; - break; - } - } - + $payment_object = $this->getEvent('invoice.payment_succeeded'); if ($payment_object) { // Now manipulate the transaction so it appears to be a failed one. $payment_object->type = 'invoice.payment_failed'; - if (class_exists('CRM_Core_Payment_StripeIPN')) { - // The $_GET['processor_id'] value is normally set by - // CRM_Core_Payment::handlePaymentMethod - $_GET['processor_id'] = $this->_paymentProcessorID; - $ipnClass = new CRM_Core_Payment_StripeIPN($payment_object); - $ipnClass->main(); - } - else { - // Deprecated method. - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($payment_object); - } + $this->ipn($payment_object); } $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); @@ -120,36 +93,10 @@ public function testIPNRecurSuccess() { $this->doPayment($payment_extra_params); // Now check to see if an event was triggered and if so, process it. - // Get all events of the type invoice.payment_succeeded that have - // happened since this code was invoked. - $params['created'] = array('gte' => $this->_created_ts); - $params['type'] = 'invoice.payment_succeeded'; - $params['ppid'] = $this->_paymentProcessorID; - - // Now try to retrieve this transaction. - $transactions = civicrm_api3('Stripe', 'listevents', $params ); - $payment_object = NULL; - foreach($transactions['values']['data'] as $transaction) { - if ($transaction->data->object->subscription == $this->_subscriptionID) { - // This is the one. - $payment_object = $transaction; - break; - } - } + $payment_object = $this->getEvent('invoice.payment_succeeded'); if ($payment_object) { - if (class_exists('CRM_Core_Payment_StripeIPN')) { - // The $_GET['processor_id'] value is normally set by - // CRM_Core_Payment::handlePaymentMethod - $_GET['processor_id'] = $this->_paymentProcessorID; - $ipnClass = new CRM_Core_Payment_StripeIPN($payment_object); - $ipnClass->main(); - } - else { - // Deprecated method. - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($payment_object); - } + $this->ipn($payment_object); } $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); $contribution_status_id = $contribution['contribution_status_id']; @@ -160,37 +107,60 @@ public function testIPNRecurSuccess() { $sub = \Stripe\Subscription::retrieve($this->_subscriptionID); $sub->cancel(); + $sub_object = $this->getEvent('customer.subscription.deleted'); + if ($sub_object) { + $this->ipn($sub_object); + } + $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); + $contribution_recur_status_id = $contribution_recur['contribution_status_id']; + $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_recur_status_id, 'name'); + $this->assertEquals('Cancelled', $status, "Recurring payment was properly cancelled via a stripe event."); + } + + /** + * Retrieve the event with a matching subscription id + */ + public function getEvent($type) { + if ($type == 'customer.subscription.deleted') { + $parameter = 'id'; + } + else { + $parameter = 'subscription'; + } + // Gather all events since this class was instantiated. $params['sk'] = $this->_sk; $params['created'] = array('gte' => $this->_created_ts); - $params['type'] = 'customer.subscription.deleted'; + $params['type'] = $type; + $params['ppid'] = $this->_paymentProcessorID; // Now try to retrieve this transaction. $transactions = civicrm_api3('Stripe', 'listevents', $params ); - $sub_object = NULL; foreach($transactions['values']['data'] as $transaction) { - if ($transaction->data->object->id == $this->_subscriptionID) { - $sub_object = $transaction; - break; + if ($transaction->data->object->$parameter == $this->_subscriptionID) { + return $transaction; } } - if ($sub_object) { - if (class_exists('CRM_Core_Payment_StripeIPN')) { - // The $_GET['processor_id'] value is normally set by - // CRM_Core_Payment::handlePaymentMethod - $_GET['processor_id'] = $this->_paymentProcessorID; - $ipnClass = new CRM_Core_Payment_StripeIPN($sub_object); - $ipnClass->main(); - } - else { - // Deprecated method. - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($sub_object); - } + return NULL; + + } + + /** + * Run the webhook/ipn + * + */ + public function ipn($data) { + if (class_exists('CRM_Core_Payment_StripeIPN')) { + // The $_GET['processor_id'] value is normally set by + // CRM_Core_Payment::handlePaymentMethod + $_GET['processor_id'] = $this->_paymentProcessorID; + $ipnClass = new CRM_Core_Payment_StripeIPN($data); + $ipnClass->main(); + } + else { + // Deprecated method. + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($data); } - $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); - $contribution_recur_status_id = $contribution_recur['contribution_status_id']; - $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_recur_status_id, 'name'); - $this->assertEquals('Cancelled', $status, "Recurring payment was properly cancelled via a stripe event."); } /** From c1b0efded73e4fbea10a5a6a6ac63de49194b25f Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 7 Jul 2017 23:09:07 -0400 Subject: [PATCH 19/30] adding test for updates to recurring contribution. --- tests/phpunit/CRM/Stripe/IpnTest.php | 107 ++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index 0ccc79e..9d270e9 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -41,6 +41,99 @@ public function setUp() { public function tearDown() { parent::tearDown(); } + /** + * Test updating a recurring contribution. + */ + public function testIPNRecurUpdate() { + $this->setupRecurringTransaction(); + $payment_extra_params = array( + 'is_recur' => 1, + 'contributionRecurID' => $this->_contributionRecurID, + 'frequency_unit' => $this->_frequency_unit, + 'frequency_interval' => $this->_frequency_interval, + 'installments' => $this->_installments + ); + $this->doPayment($payment_extra_params); + + // Now check to see if an event was triggered and if so, process it. + $payment_object = $this->getEvent('invoice.payment_succeeded'); + if ($payment_object) { + $this->ipn($payment_object); + } + + // Now that we have a recurring contribution, let's update it. + \Stripe\Stripe::setApiKey($this->_sk); + $sub = \Stripe\Subscription::retrieve($this->_subscriptionID); + + // Create a new plan if it doesn't yet exist. + $plan_id = 'every-2-month-40000-usd-test'; + + // It's possible that this test plan is still in Stripe, so try to + // retrieve it and catch the error triggered if it doesn't exist. + try { + $plan = \Stripe\Plan::retrieve($plan_id); + } + catch (Stripe\Error\InvalidRequest $e) { + // The plan has not been created yet, so create it. + $plan_details = array( + 'id' => $plan_id, + 'amount' => '40000', + 'interval' => 'month', + 'name' => "Test Updated Plan", + 'currency' => 'usd', + 'interval_count' => 2 + ); + $plan = \Stripe\Plan::create($plan_details); + + } + $sub->plan = $plan_id; + $sub->save(); + + // Now check to see if an event was triggered and if so, process it. + $payment_object = $this->getEvent('customer.subscription.updated'); + if ($payment_object) { + $this->ipn($payment_object); + } + + // Ensure the old subscription was cancelled. + $this->assertContributionRecurIsCancelled(); + + // Check for a new recurring contribution. + $params = array( + 'contact_id' => $this->_contactID, + 'amount' => '400', + 'contribution_status_id' => "In Progress", + 'return' => array('id'), + ); + $result = civicrm_api3('ContributionRecur', 'getsingle', $params); + $newContributionRecurID = $result['id']; + + // The new one should have a higher id than the old one becuase it's an + // auto increment field. + $this->assertGreaterThan($this->_contributionRecurID, $newContributionRecurID, "New recurring contribution is created on update."); + + // We should also have a new pending contribution. + $params = array( + 'contribution_recur_id' => $newContributionRecurID, + 'is_test' => 1, + 'total_amount' => '400', + 'contribution_status_id' => 'Pending', + 'return' => array('id') + ); + $newContributionID = civicrm_api3('Contribution', 'getsingle', $params); + $this->assertGreaterThan($this->_contributionID, $newContributionID, "New contribution is created on update of recurring plan."); + + // Ensure the Stripe table is updated. + $sql = "SELECT subscription_id FROM civicrm_stripe_subscriptions WHERE + contribution_recur_id = %0"; + $dao = CRM_Core_DAO::executeQuery($sql, array(0 => array($newContributionRecurID, 'Integer'))); + $dao->fetch(); + $this->assertEquals(1, $dao->N, "Stripe subscription table is updated on update to recurrig contribution"); + + // Delete the new plan so we can cleanly run the next time. + $plan->delete(); + } + /** * Test making a failed recurring contribution. */ @@ -111,6 +204,10 @@ public function testIPNRecurSuccess() { if ($sub_object) { $this->ipn($sub_object); } + $this->assertContributionRecurIsCancelled(); + } + + public function assertContributionRecurIsCancelled() { $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); $contribution_recur_status_id = $contribution_recur['contribution_status_id']; $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_recur_status_id, 'name'); @@ -121,11 +218,13 @@ public function testIPNRecurSuccess() { * Retrieve the event with a matching subscription id */ public function getEvent($type) { - if ($type == 'customer.subscription.deleted') { - $parameter = 'id'; + // If the type has subscription in it, then the id is the subscription id + if (preg_match('/\.subscription\./', $type)) { + $property = 'id'; } else { - $parameter = 'subscription'; + // Otherwise, we'll find the subscription id in the subscription property. + $property = 'subscription'; } // Gather all events since this class was instantiated. $params['sk'] = $this->_sk; @@ -136,7 +235,7 @@ public function getEvent($type) { // Now try to retrieve this transaction. $transactions = civicrm_api3('Stripe', 'listevents', $params ); foreach($transactions['values']['data'] as $transaction) { - if ($transaction->data->object->$parameter == $this->_subscriptionID) { + if ($transaction->data->object->$property == $this->_subscriptionID) { return $transaction; } } From 035cc4a550c397e87e780f2125d86311de699a0d Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 12 Jul 2017 11:16:33 -0400 Subject: [PATCH 20/30] begin work on testing memberships based on recurring contributions. --- api/v3/Stripe/Setuptest.php | 7 ++- tests/phpunit/CRM/Stripe/BaseTest.php | 66 ++++++++++++++++++++++----- tests/phpunit/CRM/Stripe/IpnTest.php | 31 ++++++++++--- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/api/v3/Stripe/Setuptest.php b/api/v3/Stripe/Setuptest.php index 3119fbe..730fa80 100644 --- a/api/v3/Stripe/Setuptest.php +++ b/api/v3/Stripe/Setuptest.php @@ -47,6 +47,11 @@ function civicrm_api3_stripe_Setuptest($params) { 'class_name' => 'Payment_Stripe', 'billing_mode' => 1 ); - $result = civicrm_api3('PaymentProcessor', 'create', $params); + // First see if it already exists. + $result = civicrm_api3('PaymentProcessor', 'get', $params); + if ($result['count'] != 1) { + // Nope, create it. + $result = civicrm_api3('PaymentProcessor', 'create', $params); + } return civicrm_api3_create_success($result['values']); } diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 6c8e627..b2ebce6 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -25,16 +25,20 @@ class CRM_Stripe_BaseTest extends \PHPUnit_Framework_TestCase implements Headles protected $_contributionID; protected $_invoiceID = 'in_19WvbKAwDouDdbFCkOnSwAN7'; protected $_financialTypeID = 1; + protected $org; + protected $_orgID; + protected $contact; protected $_contactID; protected $_contributionPageID; protected $_paymentProcessorID; protected $_paymentProcessor; protected $_trxn_id; - protected $_created_ts; + protected $_created_ts; protected $_subscriptionID; - // Secret/public keys are PTP test keys. - protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; - protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; + protected $_membershipTypeID; + // Secret/public keys are PTP test keys. + protected $_sk = 'sk_test_TlGdeoi8e1EOPC3nvcJ4q5UZ'; + protected $_pk = 'pk_test_k2hELLGpBLsOJr6jZ2z9RaYh'; protected $_cc = NULL; public function setUpHeadless() { @@ -48,10 +52,10 @@ public function setUpHeadless() { public function setUp() { parent::setUp(); require_once('stripe-php/init.php'); - $this->createPaymentProcessor(); - $this->createContact(); + $this->createPaymentProcessor(); + $this->createContact(); $this->createContributionPage(); - $this->_created_ts = time(); + $this->_created_ts = time(); $this->set_cc(); } @@ -77,7 +81,7 @@ public function tearDown() { * Create contact. */ function createContact() { - $this->contact = $this->contact = \CRM_Core_DAO::createTestObject( + $this->contact = \CRM_Core_DAO::createTestObject( 'CRM_Contact_DAO_Contact', array('contact_type' => 'Individual',) ); @@ -96,11 +100,12 @@ function createContact() { } - /** + /** * Create a stripe payment processor. * */ function createPaymentProcessor($params = array()) { + $result = civicrm_api3('Stripe', 'setuptest', $params); $processor = array_pop($result['values']); $this->_sk = $processor['user_name']; @@ -125,7 +130,7 @@ function createContributionPage($params = array()) { 'is_email_receipt' => FALSE, ), $params); $result = civicrm_api3('ContributionPage', 'create', $params); - $this->assertEquals(0, $result['is_error']); + $this->assertEquals(0, $result['is_error']); $this->_contributionPageID = $result['id']; } @@ -203,7 +208,46 @@ public function setupTransaction($params = array()) { 'payment_processor_id' => $this->_paymentProcessorID, 'is_test' => 1, ), $params)); - $this->assertEquals(0, $contribution['is_error']); + $this->assertEquals(0, $contribution['is_error']); $this->_contributionID = $contribution['id']; } + + public function createOrganization() { + if (!empty($this->_orgID)) { + return; + } + + $this->org = \CRM_Core_DAO::createTestObject( + 'CRM_Contact_DAO_Contact', + array('contact_type' => 'Organization',) + ); + $this->_orgID = $this->org->id; + } + + public function createMembershipType() { + CRM_Member_PseudoConstant::flush('membershipType'); + CRM_Core_Config::clearDBCache(); + $this->createOrganization(); + $params = array( + 'name' => 'General', + 'duration_unit' => 'year', + 'duration_interval' => 1, + 'period_type' => 'rolling', + 'member_of_contact_id' => $this->_orgID, + 'domain_id' => 1, + 'financial_type_id' => 2, + 'is_active' => 1, + 'sequential' => 1, + 'visibility' => 'Public', + ); + + $result = civicrm_api3('MembershipType', 'Create', $params); + + $this->_membershipTypeID = $result['id']; + + CRM_Member_PseudoConstant::flush('membershipType'); + CRM_Utils_Cache::singleton()->flush(); + } + + } diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index 9d270e9..d9d6079 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -25,6 +25,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { protected $_installments = 5; protected $_frequency_unit = 'month'; protected $_frequency_interval = 1; + protected $_membershipID; public function setUpHeadless() { // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). @@ -42,10 +43,26 @@ public function tearDown() { parent::tearDown(); } /** - * Test updating a recurring contribution. + * Test creating a membership related recurring contribution and + * update it after creation. */ - public function testIPNRecurUpdate() { + public function testIPNRecurMembershipUpdate() { $this->setupRecurringTransaction(); + + // Create a membership type (this will create the member org too). + $this->createMembershipType(); + + // Create the membership and link to the recurring contribution. + $params = array( + 'contact_id' => $this->_contactID, + 'membership_type_id' => $this->_membershipTypeID, + 'contribution_recur_id' => $this->_contributionRecurID, + 'format.only_id' => TRUE, + ); + $result = civicrm_api3('membership', 'create', $params); + + $this->_membershipID = $result['id']; + // Submit the payment. $payment_extra_params = array( 'is_recur' => 1, 'contributionRecurID' => $this->_contributionRecurID, @@ -228,12 +245,12 @@ public function getEvent($type) { } // Gather all events since this class was instantiated. $params['sk'] = $this->_sk; - $params['created'] = array('gte' => $this->_created_ts); + $params['created'] = array('gte' => $this->_created_ts); $params['type'] = $type; $params['ppid'] = $this->_paymentProcessorID; - // Now try to retrieve this transaction. - $transactions = civicrm_api3('Stripe', 'listevents', $params ); + // Now try to retrieve this transaction. + $transactions = civicrm_api3('Stripe', 'listevents', $params ); foreach($transactions['values']['data'] as $transaction) { if ($transaction->data->object->$property == $this->_subscriptionID) { return $transaction; @@ -268,7 +285,7 @@ public function ipn($data) { public function setupRecurringTransaction($params = array()) { $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( 'financial_type_id' => $this->_financialTypeID, - 'payment_instrument_id' => CRM_Core_OptionGroup::getValue('payment_instrument', 'Credit Card', 'name'), + 'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_ContributionRecur', 'payment_instrument_id', 'Credit Card'), 'contact_id' => $this->_contactID, 'amount' => $this->_total, 'sequential' => 1, @@ -291,7 +308,7 @@ public function setupRecurringTransaction($params = array()) { 'is_test' => 1, ), ), $params)); - $this->assertEquals(0, $contributionRecur['is_error']); + $this->assertEquals(0, $contributionRecur['is_error']); $this->_contributionRecurID = $contributionRecur['id']; $this->_contributionID = $contributionRecur['values']['0']['api.contribution.create']['id']; } From 0d0dff8d3e3eae5003385a369bd8b00c6b72f708 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 14 Jul 2017 10:54:01 -0400 Subject: [PATCH 21/30] test transfer of memberhip record to new recur cont id. --- tests/phpunit/CRM/Stripe/BaseTest.php | 33 +++++++------- tests/phpunit/CRM/Stripe/IpnTest.php | 64 ++++++++++----------------- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index b2ebce6..f11febb 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -52,8 +52,8 @@ public function setUpHeadless() { public function setUp() { parent::setUp(); require_once('stripe-php/init.php'); - $this->createPaymentProcessor(); - $this->createContact(); + $this->createPaymentProcessor(); + $this->createContact(); $this->createContributionPage(); $this->_created_ts = time(); $this->set_cc(); @@ -81,12 +81,16 @@ public function tearDown() { * Create contact. */ function createContact() { - $this->contact = \CRM_Core_DAO::createTestObject( - 'CRM_Contact_DAO_Contact', - array('contact_type' => 'Individual',) - ); - $this->_contactID = $this->contact->id; - $this->contact = $this->contact; + if (!empty($this->_contactID)) { + return; + } + $results = civicrm_api3('Contact', 'create', array( + 'contact_type' => 'Individual', + 'first_name' => 'Jose', + 'last_name' => 'Lopez' + ));; + $this->_contactID = $results['id']; + $this->contact = (Object) array_pop($results['values']); // Now we have to add an email address. $email = 'susie@example.org'; @@ -96,8 +100,6 @@ function createContact() { 'location_type_id' => 1 )); $this->contact->email = $email; - - } /** @@ -216,12 +218,11 @@ public function createOrganization() { if (!empty($this->_orgID)) { return; } - - $this->org = \CRM_Core_DAO::createTestObject( - 'CRM_Contact_DAO_Contact', - array('contact_type' => 'Organization',) - ); - $this->_orgID = $this->org->id; + $results = civicrm_api3('Contact', 'create', array( + 'contact_type' => 'Organization', + 'organization_name' => 'My Great Group' + ));; + $this->_orgID = $results['id']; } public function createMembershipType() { diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index d9d6079..df986d6 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -27,24 +27,18 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { protected $_frequency_interval = 1; protected $_membershipID; + // This test is particularly dirty for some reason so we have to + // force a reset. public function setUpHeadless() { - // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). - // See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md + $force = TRUE; return \Civi\Test::headless() ->installMe(__DIR__) - ->apply(); + ->apply($force); } - public function setUp() { - parent::setUp(); - } - - public function tearDown() { - parent::tearDown(); - } /** * Test creating a membership related recurring contribution and - * update it after creation. + * update it after creation. The membership should also be updated. */ public function testIPNRecurMembershipUpdate() { $this->setupRecurringTransaction(); @@ -56,19 +50,23 @@ public function testIPNRecurMembershipUpdate() { $params = array( 'contact_id' => $this->_contactID, 'membership_type_id' => $this->_membershipTypeID, - 'contribution_recur_id' => $this->_contributionRecurID, - 'format.only_id' => TRUE, + 'contribution_recur_id' => $this->_contributionRecurID ); $result = civicrm_api3('membership', 'create', $params); - $this->_membershipID = $result['id']; + $status = $result['values'][$this->_membershipID]['status_id']; + $this->assertEquals(1, $status, 'Membership is in new status'); + // Submit the payment. $payment_extra_params = array( 'is_recur' => 1, 'contributionRecurID' => $this->_contributionRecurID, 'frequency_unit' => $this->_frequency_unit, 'frequency_interval' => $this->_frequency_interval, - 'installments' => $this->_installments + 'installments' => $this->_installments, + 'selectMembership' => array( + 0 => $this->_membershipTypeID + ) ); $this->doPayment($payment_extra_params); @@ -83,7 +81,7 @@ public function testIPNRecurMembershipUpdate() { $sub = \Stripe\Subscription::retrieve($this->_subscriptionID); // Create a new plan if it doesn't yet exist. - $plan_id = 'every-2-month-40000-usd-test'; + $plan_id = 'membertype_1-every-2-month-40000-usd-test'; // It's possible that this test plan is still in Stripe, so try to // retrieve it and catch the error triggered if it doesn't exist. @@ -112,9 +110,6 @@ public function testIPNRecurMembershipUpdate() { $this->ipn($payment_object); } - // Ensure the old subscription was cancelled. - $this->assertContributionRecurIsCancelled(); - // Check for a new recurring contribution. $params = array( 'contact_id' => $this->_contactID, @@ -125,30 +120,17 @@ public function testIPNRecurMembershipUpdate() { $result = civicrm_api3('ContributionRecur', 'getsingle', $params); $newContributionRecurID = $result['id']; - // The new one should have a higher id than the old one becuase it's an - // auto increment field. - $this->assertGreaterThan($this->_contributionRecurID, $newContributionRecurID, "New recurring contribution is created on update."); - - // We should also have a new pending contribution. - $params = array( - 'contribution_recur_id' => $newContributionRecurID, - 'is_test' => 1, - 'total_amount' => '400', - 'contribution_status_id' => 'Pending', - 'return' => array('id') - ); - $newContributionID = civicrm_api3('Contribution', 'getsingle', $params); - $this->assertGreaterThan($this->_contributionID, $newContributionID, "New contribution is created on update of recurring plan."); - - // Ensure the Stripe table is updated. - $sql = "SELECT subscription_id FROM civicrm_stripe_subscriptions WHERE - contribution_recur_id = %0"; - $dao = CRM_Core_DAO::executeQuery($sql, array(0 => array($newContributionRecurID, 'Integer'))); - $dao->fetch(); - $this->assertEquals(1, $dao->N, "Stripe subscription table is updated on update to recurrig contribution"); + // Now ensure that the membership record is updated to have this + // new recurring contribution id. + $membership_contribution_recur_id = civicrm_api3('Membership', 'getvalue', array( + 'id' => $this->_membershipID, + 'return' => 'contribution_recur_id' + )); + $this->assertEquals($newContributionRecurID, $membership_contribution_recur_id, 'Membership is updated to new contribution recur id'); // Delete the new plan so we can cleanly run the next time. $plan->delete(); + } /** @@ -283,7 +265,7 @@ public function ipn($data) { * Create recurring contribition */ public function setupRecurringTransaction($params = array()) { - $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( + $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( 'financial_type_id' => $this->_financialTypeID, 'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_ContributionRecur', 'payment_instrument_id', 'Credit Card'), 'contact_id' => $this->_contactID, From b9f76d0418789142772ab6b69be94af6e0c3628c Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Fri, 14 Jul 2017 12:36:40 -0400 Subject: [PATCH 22/30] properly work with refactored IPN code --- tests/phpunit/CRM/Stripe/IpnTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index df986d6..8059dd9 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -154,7 +154,9 @@ public function testIPNRecurFail() { if ($payment_object) { // Now manipulate the transaction so it appears to be a failed one. $payment_object->type = 'invoice.payment_failed'; - $this->ipn($payment_object); + // Tell Ipn not to verify it - because we manipulated it. + $verify = FALSE; + $this->ipn($payment_object, $verify); } $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); @@ -246,12 +248,12 @@ public function getEvent($type) { * Run the webhook/ipn * */ - public function ipn($data) { + public function ipn($data, $verify = TRUE) { if (class_exists('CRM_Core_Payment_StripeIPN')) { // The $_GET['processor_id'] value is normally set by // CRM_Core_Payment::handlePaymentMethod $_GET['processor_id'] = $this->_paymentProcessorID; - $ipnClass = new CRM_Core_Payment_StripeIPN($data); + $ipnClass = new CRM_Core_Payment_StripeIPN($data, $verify); $ipnClass->main(); } else { From b06814a38b9343a9c6bfb822af2b6e5e59f44eac Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Mon, 17 Jul 2017 14:50:11 -0400 Subject: [PATCH 23/30] use refactored IPN if available. --- api/v3/Stripe/Ipn.php | 17 +++++++++++++---- api/v3/Stripe/Listevents.php | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index d466378..d45e091 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -63,10 +63,19 @@ function civicrm_api3_stripe_Ipn($params) { \Stripe\Stripe::setApiKey($sk); $object = \Stripe\Event::retrieve($params['evtid']); } - - $_REQUEST['ppid'] = $ppid; - $stripe = new CRM_Stripe_Page_Webhook(); - $stripe->run($object); + if (class_exists('CRM_Core_Payment_StripeIPN')) { + // The $_GET['processor_id'] value is normally set by + // CRM_Core_Payment::handlePaymentMethod + $_GET['processor_id'] = $ppid; + $ipnClass = new CRM_Core_Payment_StripeIPN($object); + $ipnClass->main(); + } + else { + // Deprecated method. + $_REQUEST['ppid'] = $ppid; + $stripe = new CRM_Stripe_Page_Webhook(); + $stripe->run($object); + } return civicrm_api3_create_success(array()); } diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index f1509c2..0617439 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -21,6 +21,8 @@ function _civicrm_api3_stripe_ListEvents_spec(&$spec) { $spec['type']['title'] = ts("Limit to the given Stripe events type"); $spec['limit']['title'] = ts("Limit number of results returned (100 is max)"); $spec['starting_after']['title'] = ts("Only return results after this event id."); + $spec['output']['default'] = 'json'; + $spec['output']['title'] = ts("How to format the output, brief or full. Defaults to full."); } /** @@ -223,7 +225,35 @@ function civicrm_api3_stripe_Listevents($params) { $err = $data_list['error']; throw new API_Exception(/*errorMessage*/ "Stripe returned an error: " . $err->message, /*errorCode*/ $err->type); } - return civicrm_api3_create_success($data_list); + $out = $data_list; + if ($params['output'] == 'brief') { + $out = array(); + foreach($data_list['data'] as $data) { + $item = array( + 'id' => $data['id'], + 'created' => date('Y-m-d H:i:s', $data['created']), + 'livemode' => $data['livemode'], + 'pending_webhooks' => $data['pending_webhooks'], + 'type' => $data['type'], + ); + if (preg_match('/invoice\.payment_/', $data['type'])) { + $item['invoice'] = $data['data']['object']->id; + $item['charge'] = $data['data']['object']->charge; + $item['customer'] = $data['data']['object']->customer; + $item['total'] = $data['data']['object']->total; + + // Check if this is in the contributions table. + $item['processed'] = 'no'; + $results = civicrm_api3('Contribution', 'get', array('trxn_id' => $item['charge'])); + if ($results['count'] > 0) { + $item['processed'] = 'yes'; + } + } + $out[] = $item; + } + } + return civicrm_api3_create_success($out); + } From b5b1ae150b2e29abbc0346bb58594ce3ec25e5bd Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 18 Jul 2017 10:48:05 -0400 Subject: [PATCH 24/30] turn off receipts if specified. --- api/v3/Stripe/Ipn.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index d45e091..ddba730 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -23,6 +23,8 @@ function _civicrm_api3_stripe_Ipn_spec(&$spec) { $spec['id']['title'] = ts("CiviCRM System Log id to replay from system log."); $spec['evtid']['title'] = ts("An event id as generated by Stripe."); $spec['ppid']['title'] = ts("The payment processor to use (required if using evtid)"); + $spec['noreceipt']['title'] = ts("Set to 1 to override contribution page settings and do not send a receipt (default is off or 0). )"); + $spec['noreceipt']['default'] = 0; } /** @@ -68,6 +70,9 @@ function civicrm_api3_stripe_Ipn($params) { // CRM_Core_Payment::handlePaymentMethod $_GET['processor_id'] = $ppid; $ipnClass = new CRM_Core_Payment_StripeIPN($object); + if ($params['noreceipt'] == 1) { + $ipnClass->is_email_receipt = 0; + } $ipnClass->main(); } else { From 09bdedec119301d8c2e93d8e58ad9c8876db8595 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 18 Jul 2017 10:49:23 -0400 Subject: [PATCH 25/30] update README to let people know about new receipt option. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ad69e0e..21067b7 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The api commands are: * id - The id from the SystemLog of the event to replay. * evtid - The Event ID as provided by Stripe. * ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one. + * noreceipt - Set to 1 if you want to suppress the generation of receipts or set to 0 or leave out to send receipts normally. TESTS ------------ From b49be0ebe0a3f3d8a5a923331212710f8007df16 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 18 Jul 2017 11:55:29 -0400 Subject: [PATCH 26/30] add subscription info to brief output. --- api/v3/Stripe/Listevents.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index 0617439..973a77b 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -240,6 +240,7 @@ function civicrm_api3_stripe_Listevents($params) { $item['invoice'] = $data['data']['object']->id; $item['charge'] = $data['data']['object']->charge; $item['customer'] = $data['data']['object']->customer; + $item['subscription'] = $data['data']['object']->subscription; $item['total'] = $data['data']['object']->total; // Check if this is in the contributions table. From a35969ba3a7c0bd41c62f2e2f7e19448e03a911b Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 18 Jul 2017 12:40:16 -0400 Subject: [PATCH 27/30] friendlier response if the Ipn has already been processed. --- api/v3/Stripe/Ipn.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index ddba730..905c326 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -24,7 +24,7 @@ function _civicrm_api3_stripe_Ipn_spec(&$spec) { $spec['evtid']['title'] = ts("An event id as generated by Stripe."); $spec['ppid']['title'] = ts("The payment processor to use (required if using evtid)"); $spec['noreceipt']['title'] = ts("Set to 1 to override contribution page settings and do not send a receipt (default is off or 0). )"); - $spec['noreceipt']['default'] = 0; + $spec['noreceipt']['api.default'] = 0; } /** @@ -65,6 +65,14 @@ function civicrm_api3_stripe_Ipn($params) { \Stripe\Stripe::setApiKey($sk); $object = \Stripe\Event::retrieve($params['evtid']); } + // Avoid a SQL error if this one has been processed already. + $sql = "SELECT COUNT(*) AS count FROM civicrm_contribution WHERE trxn_id = %0"; + $params = array(0 => array($object->data->object->charge, 'String')); + $dao = CRM_Core_DAO::executeQuery($sql, $params); + $dao->fetch(); + if ($dao->count > 0) { + return civicrm_api3_create_error("Ipn already processed."); + } if (class_exists('CRM_Core_Payment_StripeIPN')) { // The $_GET['processor_id'] value is normally set by // CRM_Core_Payment::handlePaymentMethod From f249625737b2cdc6a8bb09f8919c85a84eb412ce Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 18 Jul 2017 12:50:07 -0400 Subject: [PATCH 28/30] don't overwrite params. --- api/v3/Stripe/Ipn.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index 905c326..cd0ecec 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -67,8 +67,8 @@ function civicrm_api3_stripe_Ipn($params) { } // Avoid a SQL error if this one has been processed already. $sql = "SELECT COUNT(*) AS count FROM civicrm_contribution WHERE trxn_id = %0"; - $params = array(0 => array($object->data->object->charge, 'String')); - $dao = CRM_Core_DAO::executeQuery($sql, $params); + $sql_params = array(0 => array($object->data->object->charge, 'String')); + $dao = CRM_Core_DAO::executeQuery($sql, $sql_params); $dao->fetch(); if ($dao->count > 0) { return civicrm_api3_create_error("Ipn already processed."); From 30c6c6f7a1b964548da99894ab04e6c2b4a93bad Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 24 Oct 2017 12:54:14 -0400 Subject: [PATCH 29/30] Replacing DAO with api call https://github.com/drastik/com.drastikbydesign.stripe/issues/223 --- api/v3/Stripe/Ipn.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index cd0ecec..1724baa 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -67,10 +67,11 @@ function civicrm_api3_stripe_Ipn($params) { } // Avoid a SQL error if this one has been processed already. $sql = "SELECT COUNT(*) AS count FROM civicrm_contribution WHERE trxn_id = %0"; - $sql_params = array(0 => array($object->data->object->charge, 'String')); - $dao = CRM_Core_DAO::executeQuery($sql, $sql_params); - $dao->fetch(); - if ($dao->count > 0) { + $count_params = array( + 'trxn_id' => $object->data->object->charge, + ); + $count_result = civicrm_api3('Contribution', 'get', $count_params); + if ($count_resulst['count'] > 0) { return civicrm_api3_create_error("Ipn already processed."); } if (class_exists('CRM_Core_Payment_StripeIPN')) { From a46ae29d05af63db84cc16c0fd8e455134dd6bb2 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 24 Oct 2017 13:23:02 -0400 Subject: [PATCH 30/30] default must be preceded with api. https://github.com/drastik/com.drastikbydesign.stripe/issues/223 --- api/v3/Stripe/Listevents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v3/Stripe/Listevents.php b/api/v3/Stripe/Listevents.php index 973a77b..be8eca4 100644 --- a/api/v3/Stripe/Listevents.php +++ b/api/v3/Stripe/Listevents.php @@ -21,7 +21,7 @@ function _civicrm_api3_stripe_ListEvents_spec(&$spec) { $spec['type']['title'] = ts("Limit to the given Stripe events type"); $spec['limit']['title'] = ts("Limit number of results returned (100 is max)"); $spec['starting_after']['title'] = ts("Only return results after this event id."); - $spec['output']['default'] = 'json'; + $spec['output']['api.default'] = 'json'; $spec['output']['title'] = ts("How to format the output, brief or full. Defaults to full."); }